├── tests ├── __init__.py ├── operations_test.py ├── client_test.py ├── query_test.py └── measurement_test.py ├── influxalchemy ├── __init__.py ├── measurement.py ├── operations.py ├── client.py ├── query.py └── meta.py ├── Pipfile ├── compose.yml ├── Makefile ├── .github └── workflows │ └── pytest.yml ├── LICENSE ├── pyproject.toml ├── .gitignore ├── README.md └── notebooks └── Usage.ipynb /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /influxalchemy/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | InfluxDB Alchemy. 3 | """ 4 | 5 | from influxalchemy.client import InfluxAlchemy # noqa: F401 6 | from influxalchemy.measurement import Measurement # noqa: F401 7 | 8 | __version__ = "0.3.0" 9 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.python.org/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | [dev-packages] 7 | black = "*" 8 | flit = "*" 9 | ipdb = "*" 10 | ipython = "*" 11 | pandas = "*" 12 | pytest = "*" 13 | pytest-cov = "*" 14 | 15 | [packages] 16 | influxdb = ">= 5.0" 17 | pytz = ">= 2018.3" 18 | requests = ">= 2.20" 19 | six = ">= 1.11" 20 | -------------------------------------------------------------------------------- /compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | influxdb: 3 | image: influxdb 4 | ports: 5 | - 8086 6 | volumes: 7 | - influxdb:/var/lib/influxdb 8 | seed: 9 | image: influxdb 10 | command: | 11 | curl 'https://s3-us-west-1.amazonaws.com/noaa.water.database.0.9/NOAA_data.txt' > NOAA_data.txt 12 | influx -import -host influxdb -path=NOAA_data.txt -precision=s 13 | depends_on: 14 | - influxdb 15 | volumes: 16 | influxdb: 17 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: test build 2 | 3 | build: .venv 4 | pipenv run flit build 5 | 6 | clean: 7 | rm -rf dist 8 | 9 | ipython: 10 | pipenv run ipython 11 | 12 | publish: test build 13 | git diff HEAD --quiet 14 | pipenv run flit publish 15 | 16 | test: .venv 17 | pipenv run black --check influxalchemy tests 18 | pipenv run pytest 19 | 20 | .PHONY: all build clean ipython publish test 21 | 22 | Pipfile.lock .venv: Pipfile 23 | mkdir -p .venv 24 | pipenv install --dev 25 | touch .venv 26 | -------------------------------------------------------------------------------- /influxalchemy/measurement.py: -------------------------------------------------------------------------------- 1 | """ 2 | InfluxDB Measurement. 3 | """ 4 | 5 | import six 6 | 7 | from influxalchemy import meta 8 | 9 | 10 | # pylint: disable=too-few-public-methods 11 | class Measurement(six.with_metaclass(meta.MetaMeasurement)): 12 | """ 13 | InfluxDB Measurement. 14 | """ 15 | 16 | # pylint: disable=no-init 17 | @classmethod 18 | def new(cls, name): 19 | """ 20 | Generate new Measurement class. 21 | """ 22 | return type(name, (cls,), {"__measurement__": name}) 23 | -------------------------------------------------------------------------------- /.github/workflows/pytest.yml: -------------------------------------------------------------------------------- 1 | name: pytest 2 | on: 3 | pull_request: 4 | push: 5 | jobs: 6 | pytest: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | matrix: 10 | python: 11 | - "3.8" 12 | - "3.9" 13 | - "3.10" 14 | - "3.11" 15 | - "3.12" 16 | - "3.13" 17 | steps: 18 | - uses: actions/checkout@v4 19 | - uses: actions/setup-python@v5 20 | with: 21 | python-version: ${{ matrix.python }} 22 | - uses: amancevice/setup-code-climate@v2 23 | with: 24 | cc_test_reporter_id: ${{ secrets.CC_TEST_REPORTER_ID }} 25 | - run: cc-test-reporter before-build 26 | - run: pip install pipenv 27 | - run: make 28 | - run: cc-test-reporter after-build 29 | if: ${{ github.event_name != 'pull_request' }} 30 | -------------------------------------------------------------------------------- /tests/operations_test.py: -------------------------------------------------------------------------------- 1 | """InfluxAlchemy Operations.""" 2 | 3 | from influxalchemy.operations import Operation 4 | 5 | 6 | def test_op_init(): 7 | op = Operation(" fizz ", " buzz ") 8 | assert op._op == " fizz " 9 | assert op._nop == " buzz " 10 | 11 | 12 | def test_op_str(): 13 | op = Operation(" fizz ", " buzz ") 14 | assert str(op) == " fizz " 15 | 16 | 17 | def test_op_repr(): 18 | op = Operation(" fizz ", " buzz ") 19 | assert repr(op) == " fizz " 20 | 21 | 22 | def test_op_inv(): 23 | op = ~Operation(" fizz ", " buzz ") 24 | assert op._nop == " fizz " 25 | assert op._op == " buzz " 26 | 27 | 28 | def test_op_eq(): 29 | op0 = Operation(" fizz ", " buzz ") 30 | op1 = Operation(" fizz ", " buzz ") 31 | assert op0 == op1 32 | 33 | 34 | def test_op_ne(): 35 | op0 = Operation(" fizz ", " buzz ") 36 | op1 = ~op0 37 | assert op0 != op1 38 | -------------------------------------------------------------------------------- /influxalchemy/operations.py: -------------------------------------------------------------------------------- 1 | """ 2 | InfluxDB Operations. 3 | """ 4 | 5 | 6 | class Operation: 7 | """ 8 | InfluxDB query operation. 9 | """ 10 | 11 | def __init__(self, op, nop): 12 | self._op = op 13 | self._nop = nop 14 | 15 | def __str__(self): 16 | return self._op 17 | 18 | def __repr__(self): 19 | return str(self) 20 | 21 | def __invert__(self): 22 | return Operation(self._nop, self._op) 23 | 24 | def __eq__(self, other): 25 | return str(self) == str(other) 26 | 27 | def __ne__(self, other): 28 | return str(self) != str(other) 29 | 30 | 31 | EQ = Operation(" = ", " != ") 32 | NE = Operation(" != ", " = ") 33 | GT = Operation(" > ", " <= ") 34 | LT = Operation(" < ", " >= ") 35 | GE = Operation(" >= ", " < ") 36 | LE = Operation(" <= ", " > ") 37 | LK = Operation(" =~ ", " !~ ") 38 | NK = Operation(" !~ ", " =~ ") 39 | AND = Operation(" AND ", " OR ") 40 | OR = Operation(" OR ", " AND ") 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Alexander Mancevice 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["flit_core >=3.2,<4"] 3 | build-backend = "flit_core.buildapi" 4 | 5 | [project] 6 | authors = [ 7 | { name = "Alexander Mancevice", email = "alexander.mancevice@hey.com" }, 8 | ] 9 | classifiers = [ 10 | "Development Status :: 3 - Alpha", 11 | "Intended Audience :: Developers", 12 | "Intended Audience :: System Administrators", 13 | "License :: OSI Approved :: MIT License", 14 | "License :: OSI Approved :: MIT License", 15 | "Operating System :: OS Independent", 16 | "Operating System :: OS Independent", 17 | "Programming Language :: Python :: 3", 18 | "Programming Language :: Python :: 3.8", 19 | "Programming Language :: Python :: 3.9", 20 | "Programming Language :: Python :: 3.10", 21 | "Programming Language :: Python :: 3.11", 22 | "Programming Language :: Python :: 3.12", 23 | "Programming Language :: Python :: 3.13", 24 | "Topic :: Utilities", 25 | ] 26 | dependencies = [ 27 | "influxdb >= 5.0", 28 | "pytz >= 2018.3", 29 | "requests >= 2.20", 30 | "six >= 1.11", 31 | ] 32 | dynamic = ["version", "description"] 33 | license = { file = "LICENSE" } 34 | name = "influxalchemy" 35 | requires-python = ">= 3.8" 36 | readme = "README.md" 37 | 38 | [project.urls] 39 | Home = "https://github.com/amancevice/knackhq" 40 | 41 | [tool.pytest.ini_options] 42 | minversion = "6.0" 43 | addopts = "--cov influxalchemy --cov tests --cov-report term-missing --cov-report xml" 44 | -------------------------------------------------------------------------------- /influxalchemy/client.py: -------------------------------------------------------------------------------- 1 | """ 2 | InfluxAlchemy Client. 3 | """ 4 | 5 | from influxalchemy import query 6 | from influxalchemy.measurement import Measurement 7 | 8 | 9 | class InfluxAlchemy: 10 | """ 11 | InfluxAlchemy database session. 12 | 13 | client (InfluxDBClient): Connection to InfluxDB database 14 | """ 15 | 16 | def __init__(self, client): 17 | self.bind = client 18 | # pylint: disable=protected-access 19 | assert ( 20 | self.bind._database is not None 21 | ), "InfluxDB client database cannot be None" 22 | 23 | def query(self, *entities): 24 | """ 25 | Query InfluxDB entities. Entities are either Measurements or 26 | Tags/Fields. 27 | """ 28 | return query.InfluxDBQuery(entities, self) 29 | 30 | def measurements(self): 31 | """ 32 | Get measurements of an InfluxDB. 33 | """ 34 | results = self.bind.query("SHOW MEASUREMENTS;") 35 | for res in results.get_points(): 36 | yield Measurement.new(str(res["name"])) 37 | 38 | def tags(self, measurement): 39 | """ 40 | Get tags of a measurements in InfluxDB. 41 | """ 42 | tags = self.bind.query("SHOW tag keys FROM %s" % measurement) 43 | pts = sorted(set(t["tagKey"] for t in tags.get_points())) 44 | return pts 45 | 46 | def fields(self, measurement): 47 | """ 48 | Get fields of a measurements in InfluxDB. 49 | """ 50 | fields = self.bind.query("SHOW field keys FROM %s" % measurement) 51 | pts = sorted(set(f["fieldKey"] for f in fields.get_points())) 52 | return pts 53 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Directory-based project format: 2 | .idea/ 3 | **/.idea/ 4 | */.idea 5 | */.idea/** 6 | 7 | # Byte-compiled / optimized / DLL files 8 | __pycache__/ 9 | *.py[cod] 10 | *$py.class 11 | 12 | # C extensions 13 | *.so 14 | 15 | # Distribution / packaging 16 | .Python 17 | env/ 18 | build/ 19 | develop-eggs/ 20 | dist/ 21 | downloads/ 22 | eggs/ 23 | .eggs/ 24 | lib/ 25 | lib64/ 26 | parts/ 27 | sdist/ 28 | var/ 29 | *.egg-info/ 30 | .installed.cfg 31 | *.egg 32 | 33 | # PyInstaller 34 | # Usually these files are written by a python script from a template 35 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 36 | *.manifest 37 | *.spec 38 | 39 | # Installer logs 40 | pip-log.txt 41 | pip-delete-this-directory.txt 42 | 43 | # Unit test / coverage reports 44 | htmlcov/ 45 | .tox/ 46 | .coverage 47 | .coverage.* 48 | .coveragerc 49 | .cache 50 | nosetests.xml 51 | coverage.xml 52 | *,cover 53 | .hypothesis/ 54 | 55 | # Translations 56 | *.mo 57 | *.pot 58 | 59 | # Django stuff: 60 | *.log 61 | local_settings.py 62 | 63 | # Flask stuff: 64 | instance/ 65 | .webassets-cache 66 | 67 | # Scrapy stuff: 68 | .scrapy 69 | 70 | # Sphinx documentation 71 | docs/_build/ 72 | 73 | # PyBuilder 74 | target/ 75 | 76 | # IPython Notebook 77 | .ipynb_checkpoints 78 | 79 | # pyenv 80 | .python-version 81 | 82 | # celery beat schedule file 83 | celerybeat-schedule 84 | 85 | # dotenv 86 | .env 87 | 88 | # virtualenv 89 | venv/ 90 | ENV/ 91 | 92 | # Spyder project settings 93 | .spyderproject 94 | 95 | # Rope project settings 96 | .ropeproject 97 | 98 | # pytest 99 | .pytest_cache/ 100 | 101 | Pipfile.lock 102 | -------------------------------------------------------------------------------- /tests/client_test.py: -------------------------------------------------------------------------------- 1 | """InfluxAlchemy client tests.""" 2 | 3 | from unittest import mock 4 | 5 | import influxdb 6 | from influxalchemy.client import InfluxAlchemy 7 | from influxalchemy.measurement import Measurement 8 | 9 | 10 | @mock.patch("influxdb.InfluxDBClient") 11 | def test_query(mock_flux): 12 | db = influxdb.InfluxDBClient(database="fizz") 13 | db.query.side_effect = influxdb.exceptions.InfluxDBClientError(None) 14 | client = InfluxAlchemy(db) 15 | query = client.query(Measurement.new("buzz")) 16 | assert str(query) == "SELECT * FROM buzz;" 17 | 18 | 19 | @mock.patch("influxdb.InfluxDBClient.query") 20 | def test_measurements(mock_flux): 21 | mock_res = mock.MagicMock() 22 | mock_res.get_points.return_value = [{"name": "fizz"}] 23 | mock_flux.return_value = mock_res 24 | db = influxdb.InfluxDBClient(database="fizz") 25 | client = InfluxAlchemy(db) 26 | list(client.measurements()) 27 | mock_flux.assert_called_once_with("SHOW MEASUREMENTS;") 28 | 29 | 30 | @mock.patch("influxdb.InfluxDBClient.query") 31 | def test_tags(mock_flux): 32 | mock_res = mock.MagicMock() 33 | mock_res.get_points.return_value = [{"tagKey": "sensor_id"}] 34 | mock_flux.return_value = mock_res 35 | db = influxdb.InfluxDBClient(database="fizz") 36 | client = InfluxAlchemy(db) 37 | assert client.tags(Measurement.new("environment")) == ["sensor_id"] 38 | 39 | 40 | @mock.patch("influxdb.InfluxDBClient.query") 41 | def test_fields(mock_flux): 42 | mock_res = mock.MagicMock() 43 | mock_res.get_points.return_value = [ 44 | {"fieldKey": "humidity", "fieldType": "float"}, 45 | {"fieldKey": "temperature", "fieldType": "float"}, 46 | ] 47 | mock_flux.return_value = mock_res 48 | db = influxdb.InfluxDBClient(database="fizz") 49 | client = InfluxAlchemy(db) 50 | exp = ["humidity", "temperature"] 51 | assert client.fields(Measurement.new("environment")) == exp 52 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # InfluxAlchemy 2 | 3 | ![pypi](https://img.shields.io/pypi/v/influxalchemy?color=yellow&logo=python&logoColor=eee&style=flat-square) 4 | ![python](https://img.shields.io/pypi/pyversions/influxalchemy?logo=python&logoColor=eee&style=flat-square) 5 | [![pytest](https://img.shields.io/github/actions/workflow/status/amancevice/influxalchemy/pytest.yml?logo=github&style=flat-square)](https://github.com/amancevice/influxalchemy/actions/workflows/pytest.yml) 6 | [![coverage](https://img.shields.io/codeclimate/coverage/amancevice/influxalchemy?logo=code-climate&style=flat-square)](https://codeclimate.com/github/amancevice/influxalchemy/test_coverage) 7 | [![maintainability](https://img.shields.io/codeclimate/maintainability/amancevice/influxalchemy?logo=code-climate&style=flat-square)](https://codeclimate.com/github/amancevice/influxalchemy/maintainability) 8 | 9 | Query InfluxDB using SQLAlchemy-style syntax 10 | 11 | ## Installation 12 | 13 | ```bash 14 | pip install influxalchemy 15 | ``` 16 | 17 | ## Usage 18 | 19 | ```python 20 | import influxdb 21 | import influxalchemy 22 | ``` 23 | 24 | ### Define InfluxAlchemy Measurements 25 | 26 | ```python 27 | class Widgets(influxalchemy.Measurement): 28 | __measurement__ = 'widgets' 29 | 30 | 31 | class Wombats(influxalchemy.Measurement): 32 | __measurement__ = 'wombats' 33 | ``` 34 | 35 | The class-attribute `__measurement__` can be omitted and will default to the class name if absent. 36 | 37 | ### Open InfluxAlchemy Connection 38 | 39 | ```python 40 | db = influxdb.DataFrameClient(database="example") 41 | flux = influxalchemy.InfluxAlchemy(db) 42 | ``` 43 | 44 | ## Query InfluxDB 45 | 46 | ### Query Single Measurement 47 | 48 | ```python 49 | # SELECT * FROM widgets; 50 | flux.query(Widgets) 51 | ``` 52 | 53 | ### Query Ad Hoc Measurement 54 | 55 | ```python 56 | # SELECT * from /.*/; 57 | flux.query(influxalchemy.Measurement.new("/.*/")) 58 | ``` 59 | 60 | ### Select Fields of Measurement 61 | 62 | ```python 63 | # SELECT tag1, field2 FROM widgets; 64 | flux.query(Widgets.tag1, Widgets.field2) 65 | ``` 66 | 67 | ### Query Across Measurements 68 | 69 | ```python 70 | # SELECT * FROM /widgets|wombats/; 71 | flux.query(Widgets | Wombats) 72 | ``` 73 | 74 | ### Filter Tags 75 | 76 | ```python 77 | # SELECT * FROM widgets WHERE tag1 = 'fizz'; 78 | flux.query(Widgets).filter(Widgets.tag1 == "fizz") 79 | ``` 80 | 81 | ### Filter Tags with 'like' 82 | 83 | ```python 84 | # SELECT * FROM widgets WHERE tag1 =~ /z$/; 85 | flux.query(Widgets).filter(Widgets.tag1.like("/z$/")) 86 | ``` 87 | 88 | ### Chain Filters 89 | 90 | ```python 91 | clause1 = Widgets.tag1 == "fizz" 92 | clause2 = Widgets.tag2 == "buzz" 93 | 94 | # SELECT * FROM widgets WHERE tag1 = 'fizz' AND tag2 = 'buzz'; 95 | flux.query(Widgets).filter(clause1 & clause2) 96 | 97 | # SELECT * FROM widgets WHERE tag1 = 'fizz' OR tag2 = 'buzz'; 98 | flux.query(Widgets).filter(clause1 | clause2) 99 | ``` 100 | 101 | ### Group By 102 | 103 | ```python 104 | # SELECT * FROM widgets GROUP BY time(1d); 105 | flux.query(Widgets).group_by("time(1d)") 106 | 107 | # SELECT * FROM widgets GROUP BY tag1; 108 | flux.query(Widgets).group_by(Widgets.tag1) 109 | ``` 110 | 111 | ### Time 112 | 113 | ```python 114 | # SELECT * FROM widgets WHERE (time > now() - 7d); 115 | flux.query(Widgets).filter(Widgets.time > "now() - 7d") 116 | 117 | # SELECT * FROM widgets WHERE time >= '2016-01-01' AND time <= now() - 7d; 118 | d = date(2016, 1, 1) 119 | flux.query(Widgets).filter(Widgets.time.between(d, "now() - 7d")) 120 | ``` 121 | 122 | Note that naive datetime object will be assumed in UTC timezone. 123 | -------------------------------------------------------------------------------- /influxalchemy/query.py: -------------------------------------------------------------------------------- 1 | """ 2 | InfluxDB Query Object. 3 | """ 4 | 5 | import functools 6 | 7 | from influxalchemy import meta 8 | 9 | 10 | class InfluxDBQuery: 11 | """ 12 | InfluxDB Query object. 13 | 14 | entities (tuple): Query entities 15 | client (InfluxAlchemy): InfluxAlchemy instance 16 | expressions (tuple): Query filters 17 | groupby (str): GROUP BY string 18 | limit (int): LIMIT int 19 | """ 20 | 21 | def __init__(self, entities, client, expressions=None, groupby=None, limit=None): 22 | # pylint: disable=too-many-arguments 23 | self._entities = entities 24 | self._client = client 25 | self._expressions = expressions or () 26 | self._groupby = groupby 27 | self._limit = limit 28 | 29 | def __str__(self): 30 | select = ", ".join(self._select) 31 | from_ = self._from 32 | where = " AND ".join(self._where) 33 | if any(where): 34 | iql = "SELECT %s FROM %s WHERE %s" % (select, from_, where) 35 | else: 36 | iql = "SELECT %s FROM %s" % (select, from_) 37 | if self._groupby is not None: 38 | iql += " GROUP BY %s" % self._groupby 39 | if self._limit is not None: 40 | iql += " LIMIT {0}".format(self._limit) 41 | return "%s;" % iql 42 | 43 | def __repr__(self): 44 | return str(self) 45 | 46 | def execute(self): 47 | """ 48 | Execute query. 49 | """ 50 | return self._client.bind.query(str(self)) 51 | 52 | def filter(self, *expressions): 53 | """ 54 | Filter query. 55 | """ 56 | expressions = self._expressions + expressions 57 | return InfluxDBQuery(self._entities, self._client, expressions=expressions) 58 | 59 | def filter_by(self, **kwargs): 60 | """ 61 | Filter query by tag value. 62 | """ 63 | expressions = self._expressions 64 | for key, val in sorted(kwargs.items()): 65 | expressions += (meta.TagExp.equals(key, val),) 66 | return InfluxDBQuery(self._entities, self._client, expressions=expressions) 67 | 68 | def group_by(self, groupby): 69 | """ 70 | Group query. 71 | """ 72 | return InfluxDBQuery(self._entities, self._client, self._expressions, groupby) 73 | 74 | def limit(self, limit): 75 | """ 76 | Limit query 77 | """ 78 | assert isinstance(limit, int) 79 | return InfluxDBQuery( 80 | self._entities, self._client, self._expressions, self._groupby, limit 81 | ) 82 | 83 | @property 84 | def measurement(self): 85 | """ 86 | Query measurement. 87 | """ 88 | measurements = set(x.measurement for x in self._entities) 89 | return functools.reduce(lambda x, y: x | y, measurements) 90 | 91 | @property 92 | def _select(self): 93 | """ 94 | SELECT statement. 95 | """ 96 | selects = [] 97 | for ent in self._entities: 98 | # Entity is a Tag 99 | if isinstance(ent, meta.Tag): 100 | selects.append(str(ent)) 101 | # Entity is a Measurement 102 | else: 103 | try: 104 | for tag in self._client.tags(ent): 105 | selects.append(tag) 106 | for field in self._client.fields(ent): 107 | selects.append(field) 108 | # pylint: disable=broad-except 109 | except Exception: 110 | pass 111 | return selects or ["*"] 112 | 113 | @property 114 | def _from(self): 115 | """ 116 | FROM statement. 117 | """ 118 | return str(self.measurement) 119 | 120 | @property 121 | def _where(self): 122 | """ 123 | WHERE statement. 124 | """ 125 | for exp in self._expressions: 126 | yield "(%s)" % exp 127 | -------------------------------------------------------------------------------- /tests/query_test.py: -------------------------------------------------------------------------------- 1 | """InfluxAlchemy Query Tests.""" 2 | 3 | import sys 4 | from unittest import mock 5 | from datetime import date 6 | from datetime import datetime 7 | from datetime import timedelta 8 | from datetime import timezone 9 | 10 | import influxdb 11 | from influxalchemy.client import InfluxAlchemy 12 | from influxalchemy.measurement import Measurement 13 | 14 | 15 | @mock.patch("influxdb.InfluxDBClient.query") 16 | def test_repr(mock_qry): 17 | mock_qry.side_effect = influxdb.exceptions.InfluxDBClientError(None) 18 | db = influxdb.InfluxDBClient(database="example") 19 | client = InfluxAlchemy(db) 20 | query = client.query(Measurement.new("fizz")) 21 | assert repr(query) == "SELECT * FROM fizz;" 22 | 23 | 24 | @mock.patch("influxdb.InfluxDBClient.query") 25 | def test_execute(mock_qry): 26 | db = influxdb.InfluxDBClient(database="example") 27 | client = InfluxAlchemy(db) 28 | query = client.query(Measurement.new("fizz").buzz) 29 | query.execute() 30 | mock_qry.assert_called_with("SELECT buzz FROM fizz;") 31 | 32 | 33 | @mock.patch("influxdb.InfluxDBClient.query") 34 | def test_filter(mock_qry): 35 | mock_qry.side_effect = influxdb.exceptions.InfluxDBClientError(None) 36 | db = influxdb.InfluxDBClient(database="example") 37 | client = InfluxAlchemy(db) 38 | meas = Measurement.new("fizz") 39 | query = client.query(meas).filter(meas.buzz == "goo") 40 | assert repr(query) == "SELECT * FROM fizz WHERE (buzz = 'goo');" 41 | 42 | 43 | @mock.patch("influxdb.InfluxDBClient.query") 44 | def test_filter_time_naive(mock_qry): 45 | mock_qry.side_effect = influxdb.exceptions.InfluxDBClientError(None) 46 | db = influxdb.InfluxDBClient(database="example") 47 | client = InfluxAlchemy(db) 48 | meas = Measurement.new("fizz") 49 | d = datetime(2016, 10, 1) 50 | query = client.query(meas).filter(meas.time >= d) 51 | assert ( 52 | repr(query) == "SELECT * FROM fizz WHERE (time >= '2016-10-01T00:00:00+00:00');" 53 | ) 54 | 55 | 56 | @mock.patch("influxdb.InfluxDBClient.query") 57 | def test_filter_time_date(mock_qry): 58 | mock_qry.side_effect = influxdb.exceptions.InfluxDBClientError(None) 59 | db = influxdb.InfluxDBClient(database="example") 60 | client = InfluxAlchemy(db) 61 | meas = Measurement.new("fizz") 62 | d = date(2016, 10, 1) 63 | query = client.query(meas).filter(meas.time >= d) 64 | assert repr(query) == "SELECT * FROM fizz WHERE (time >= '2016-10-01');" 65 | 66 | 67 | @mock.patch("influxdb.InfluxDBClient.query") 68 | def test_filter_time_aware(mock_qry): 69 | mock_qry.side_effect = influxdb.exceptions.InfluxDBClientError(None) 70 | db = influxdb.InfluxDBClient(database="example") 71 | client = InfluxAlchemy(db) 72 | meas = Measurement.new("fizz") 73 | if sys.version_info.major >= 3: 74 | tz_vietnam = timezone(timedelta(hours=7, minutes=7)) 75 | else: 76 | tz_vietnam = timezone("Asia/Ho_Chi_Minh") 77 | d_low = datetime(2016, 9, 1, tzinfo=tz_vietnam) 78 | d_high = datetime(2016, 10, 2, 8) 79 | query = client.query(meas).filter(meas.time.between(d_low, d_high)) 80 | assert ( 81 | repr(query) == "SELECT * FROM fizz WHERE (time >= '2016-09-01T00:00:00+07:07' " 82 | "AND time <= '2016-10-02T08:00:00+00:00');" 83 | ) 84 | 85 | 86 | @mock.patch("influxdb.InfluxDBClient.query") 87 | def test_group_by(mock_qry): 88 | mock_qry.side_effect = influxdb.exceptions.InfluxDBClientError(None) 89 | db = influxdb.InfluxDBClient(database="example") 90 | client = InfluxAlchemy(db) 91 | query = client.query(Measurement.new("fizz")).group_by("buzz") 92 | assert str(query) == "SELECT * FROM fizz GROUP BY buzz;" 93 | 94 | 95 | @mock.patch("influxdb.InfluxDBClient.query") 96 | def test_filter_by(mock_qry): 97 | mock_qry.side_effect = influxdb.exceptions.InfluxDBClientError(None) 98 | db = influxdb.InfluxDBClient(database="example") 99 | client = InfluxAlchemy(db) 100 | query = client.query(Measurement.new("fizz")).filter_by(buzz="goo") 101 | assert str(query) == "SELECT * FROM fizz WHERE (buzz = 'goo');" 102 | 103 | 104 | def test_tags(): 105 | db = influxdb.InfluxDBClient(database="example") 106 | client = InfluxAlchemy(db) 107 | fizz = Measurement.new("fizz") 108 | query = client.query(fizz.buzz, fizz.bug) 109 | assert str(query) == "SELECT buzz, bug FROM fizz;" 110 | 111 | 112 | @mock.patch("influxalchemy.InfluxAlchemy.tags") 113 | @mock.patch("influxalchemy.InfluxAlchemy.fields") 114 | def test_get_tags_fields(mock_fields, mock_tags): 115 | mock_tags.return_value = ["fizz", "buzz"] 116 | mock_fields.return_value = ["foo", "goo"] 117 | db = influxdb.InfluxDBClient(database="example") 118 | client = InfluxAlchemy(db) 119 | fizz = Measurement.new("fuzz") 120 | query = client.query(fizz) 121 | assert str(query) == "SELECT fizz, buzz, foo, goo FROM fuzz;" 122 | 123 | 124 | @mock.patch("influxalchemy.InfluxAlchemy.tags") 125 | @mock.patch("influxalchemy.InfluxAlchemy.fields") 126 | def test_get_empty_tags_fields(mock_fields, mock_tags): 127 | mock_tags.return_value = [] 128 | mock_fields.return_value = [] 129 | db = influxdb.InfluxDBClient(database="example") 130 | client = InfluxAlchemy(db) 131 | fizz = Measurement.new("fuzz") 132 | query = client.query(fizz) 133 | assert str(query) == "SELECT * FROM fuzz;" 134 | 135 | 136 | @mock.patch("influxdb.InfluxDBClient.query") 137 | def test_limit_1(mock_limit): 138 | db = influxdb.InfluxDBClient(database="example") 139 | client = InfluxAlchemy(db) 140 | fizz = Measurement.new("fuzz") 141 | query = client.query(fizz).limit(1) 142 | assert str(query) == "SELECT * FROM fuzz LIMIT 1;" 143 | 144 | 145 | @mock.patch("influxdb.InfluxDBClient.query") 146 | def test_limit_2(mock_limit): 147 | db = influxdb.InfluxDBClient(database="example") 148 | client = InfluxAlchemy(db) 149 | fizz = Measurement.new("fuzz") 150 | query = client.query(fizz).filter_by(vendor="quandl", market="XCME") 151 | assert ( 152 | str(query) 153 | == "SELECT * FROM fuzz WHERE (market = 'XCME') AND (vendor = 'quandl');" 154 | ) 155 | 156 | 157 | @mock.patch("influxdb.InfluxDBClient.query") 158 | def test_limit_3(mock_limit): 159 | db = influxdb.InfluxDBClient(database="example") 160 | client = InfluxAlchemy(db) 161 | fizz = Measurement.new("fuzz") 162 | query = ( 163 | client.query(fizz).filter(fizz.foo == "123").filter(fizz.boo == "555").limit(2) 164 | ) 165 | assert ( 166 | str(query) 167 | == "SELECT * FROM fuzz WHERE (foo = '123') AND (boo = '555') LIMIT 2;" 168 | ) 169 | -------------------------------------------------------------------------------- /tests/measurement_test.py: -------------------------------------------------------------------------------- 1 | """InfluxAlchemy Measurements.""" 2 | 3 | from datetime import datetime 4 | 5 | from influxalchemy.measurement import Measurement 6 | from influxalchemy.meta import Tag 7 | from influxalchemy.meta import TagExp 8 | from influxalchemy.operations import EQ 9 | from influxalchemy.operations import NE 10 | from influxalchemy.operations import GT 11 | from influxalchemy.operations import LT 12 | from influxalchemy.operations import GE 13 | from influxalchemy.operations import LE 14 | from influxalchemy.operations import LK 15 | from influxalchemy.operations import NK 16 | 17 | 18 | def test_meta_getattr(): 19 | meas = Measurement.new("fizz") 20 | assert meas.buzz == Tag("buzz", meas) 21 | 22 | 23 | def test_meta_str(): 24 | meas = Measurement.new("fizz") 25 | assert str(meas) == "fizz" 26 | 27 | 28 | def test_meta_ne(): 29 | meas0 = Measurement.new("fizz") 30 | meas1 = Measurement.new("buzz") 31 | assert meas0 != meas1 32 | 33 | 34 | def test_meta_or(): 35 | meas0 = Measurement.new("fizz") 36 | meas1 = Measurement.new("buzz") 37 | assert (meas0 | meas1) == Measurement.new("/fizz|buzz/") 38 | 39 | 40 | def test_meta_measurement(): 41 | meas = Measurement.new("fizz") 42 | assert meas == meas.measurement 43 | 44 | 45 | class Fizz(Measurement): 46 | __measurement__ = "fizz" 47 | 48 | 49 | def test_new(): 50 | assert Measurement.new("fizz") == Fizz 51 | 52 | 53 | def test_tag_init(): 54 | meas = Measurement.new("fizz") 55 | tag = Tag("buzz", meas) 56 | assert tag == meas.buzz 57 | 58 | 59 | def test_tag_str(): 60 | meas = Measurement.new("fizz") 61 | tag = Tag("buzz", meas) 62 | assert str(tag) == "buzz" 63 | 64 | 65 | def test_tag_repr(): 66 | meas = Measurement.new("fizz") 67 | tag = Tag("buzz", meas) 68 | assert repr(tag) == "" 69 | 70 | 71 | def test_tag_eq(): 72 | meas = Measurement.new("fizz") 73 | tag = Tag("buzz", meas) 74 | exp = tag == "foo" 75 | assert exp == TagExp("buzz", EQ, "foo") 76 | 77 | 78 | def test_tag_ne(): 79 | meas = Measurement.new("fizz") 80 | tag = Tag("buzz", meas) 81 | exp = tag != "foo" 82 | assert exp == TagExp("buzz", NE, "foo") 83 | 84 | 85 | def test_tag_gt(): 86 | meas = Measurement.new("fizz") 87 | tag = Tag("buzz", meas) 88 | exp = tag > "foo" 89 | assert exp == TagExp("buzz", GT, "foo") 90 | 91 | 92 | def test_tag_lt(): 93 | meas = Measurement.new("fizz") 94 | tag = Tag("buzz", meas) 95 | exp = tag < "foo" 96 | assert exp == TagExp("buzz", LT, "foo") 97 | 98 | 99 | def test_tag_ge(): 100 | meas = Measurement.new("fizz") 101 | tag = Tag("buzz", meas) 102 | exp = tag >= "foo" 103 | assert exp == TagExp("buzz", GE, "foo") 104 | 105 | 106 | def test_tag_le(): 107 | meas = Measurement.new("fizz") 108 | tag = Tag("buzz", meas) 109 | exp = tag <= "foo" 110 | assert exp == TagExp("buzz", LE, "foo") 111 | 112 | 113 | def test_tag_like(): 114 | meas = Measurement.new("fizz") 115 | tag = Tag("buzz", meas) 116 | exp = tag.like("foo") 117 | assert exp == TagExp("buzz", LK, "foo") 118 | 119 | 120 | def test_tag_notlike(): 121 | meas = Measurement.new("fizz") 122 | tag = Tag("buzz", meas) 123 | exp = tag.notlike("foo") 124 | assert exp == TagExp("buzz", NK, "foo") 125 | 126 | 127 | def test_time_between(): 128 | meas = Measurement.new("fizz") 129 | exp = meas.time.between("'2016-01-01'", "now() - 7d") 130 | assert exp == TagExp(meas.time, " >= ", "'2016-01-01'") & TagExp( 131 | meas.time, " <= ", "now() - 7d" 132 | ) 133 | 134 | 135 | def test_time_between_excl(): 136 | meas = Measurement.new("fizz") 137 | exp = meas.time.between("'2016-01-01'", "now() - 7d", False, False) 138 | assert exp == TagExp(meas.time, " > ", "'2016-01-01'") & TagExp( 139 | meas.time, " < ", "now() - 7d" 140 | ) 141 | 142 | 143 | def test_time_between_dt(): 144 | meas = Measurement.new("fizz") 145 | d = datetime(2016, 1, 1) 146 | exp = meas.time.between(d, "now() - 7d") 147 | assert exp == TagExp(meas.time, " >= ", d) & TagExp(meas.time, " <= ", "now() - 7d") 148 | 149 | 150 | def test_exp_init(): 151 | meas = Measurement.new("fizz") 152 | exp = TagExp(meas.buzz, " = ", "goo") 153 | assert exp._left == meas.buzz 154 | assert exp._op == " = " 155 | assert exp._right == "'goo'" 156 | 157 | 158 | def test_exp_str(): 159 | meas = Measurement.new("fizz") 160 | exp = TagExp(meas.buzz, " = ", "goo") 161 | assert str(exp) == "buzz = 'goo'" 162 | 163 | 164 | def test_exp_repr(): 165 | meas = Measurement.new("fizz") 166 | exp = TagExp(meas.buzz, " = ", "goo") 167 | assert repr(exp) == "[ buzz = 'goo' ]" 168 | 169 | 170 | def test_exp_ne(): 171 | meas = Measurement.new("fizz") 172 | exp0 = TagExp(meas.buzz, " = ", "goo") 173 | exp1 = TagExp(meas.guzz, " = ", "zoo") 174 | assert exp0 != exp1 175 | 176 | 177 | def test_exp_and(): 178 | meas = Measurement.new("fizz") 179 | exp0 = TagExp(meas.buzz, " = ", "goo") 180 | exp1 = TagExp(meas.guzz, " = ", "zoo") 181 | assert (exp0 & exp1) == TagExp("buzz = 'goo'", " AND ", "guzz = 'zoo'") 182 | 183 | 184 | def test_exp_or(): 185 | meas = Measurement.new("fizz") 186 | exp0 = TagExp(meas.buzz, " = ", "goo") 187 | exp1 = TagExp(meas.guzz, " = ", "zoo") 188 | assert (exp0 | exp1) == TagExp("buzz = 'goo'", " OR ", "guzz = 'zoo'") 189 | 190 | 191 | def test_exp_inv(): 192 | meas = Measurement.new("fizz") 193 | exp = TagExp(meas.buzz, EQ, "goo") 194 | assert ~exp == TagExp(meas.buzz, NE, "'goo'") 195 | 196 | 197 | def test_equals(): 198 | meas = Measurement.new("fizz") 199 | exp = TagExp.equals(meas.buzz, "goo") 200 | assert exp == TagExp(meas.buzz, EQ, "goo") 201 | 202 | 203 | def test_notequals(): 204 | meas = Measurement.new("fizz") 205 | exp = TagExp.notequals(meas.buzz, "goo") 206 | assert exp == TagExp(meas.buzz, NE, "goo") 207 | 208 | 209 | def test_greater_than(): 210 | meas = Measurement.new("fizz") 211 | exp = TagExp.greater_than(meas.buzz, "goo") 212 | assert exp == TagExp(meas.buzz, GT, "goo") 213 | 214 | 215 | def test_less_than(): 216 | meas = Measurement.new("fizz") 217 | exp = TagExp.less_than(meas.buzz, "goo") 218 | assert exp == TagExp(meas.buzz, LT, "goo") 219 | 220 | 221 | def test_greater_equal(): 222 | meas = Measurement.new("fizz") 223 | exp = TagExp.greater_equal(meas.buzz, "goo") 224 | assert exp == TagExp(meas.buzz, GE, "goo") 225 | 226 | 227 | def test_less_equal(): 228 | meas = Measurement.new("fizz") 229 | exp = TagExp.less_equal(meas.buzz, "goo") 230 | assert exp == TagExp(meas.buzz, LE, "goo") 231 | 232 | 233 | def test_like(): 234 | meas = Measurement.new("fizz") 235 | exp = TagExp.like(meas.buzz, "goo") 236 | assert exp == TagExp(meas.buzz, LK, "goo") 237 | 238 | 239 | def test_notlike(): 240 | meas = Measurement.new("fizz") 241 | exp = TagExp.notlike(meas.buzz, "goo") 242 | assert exp == TagExp(meas.buzz, NK, "goo") 243 | -------------------------------------------------------------------------------- /influxalchemy/meta.py: -------------------------------------------------------------------------------- 1 | """ 2 | InfluxDB Meta Measurement. 3 | """ 4 | 5 | from datetime import date 6 | 7 | from influxalchemy import operations 8 | 9 | try: 10 | from datetime import timezone 11 | 12 | UTC = timezone.utc # pragma: no cover 13 | except ImportError: # pragma: no cover 14 | import pytz # pragma: no cover 15 | 16 | UTC = pytz.utc # pragma: no cover 17 | 18 | 19 | def make_tz_aware(datetime_obj): 20 | """ 21 | Make a date/datetime object to be timezone-aware. 22 | """ 23 | # 'date' object doesn't need to be timezone aware 24 | # pylint: disable=unidiomatic-typecheck 25 | if type(datetime_obj) is date: 26 | return datetime_obj 27 | # Already aware 28 | if datetime_obj.tzinfo: 29 | return datetime_obj 30 | # With naive datetime object, assume it is UTC 31 | return datetime_obj.replace(tzinfo=UTC) 32 | 33 | 34 | class MetaMeasurement(type): 35 | """ 36 | Meta class of Measurement. 37 | """ 38 | 39 | def __new__(cls, name, bases, dict_): 40 | dict_.setdefault("__measurement__", name) 41 | return super(MetaMeasurement, cls).__new__(cls, name, bases, dict_) 42 | 43 | def __getattribute__(cls, name): 44 | try: 45 | return super(MetaMeasurement, cls).__getattribute__(name) 46 | except AttributeError: 47 | if name == "time": 48 | return Time(name, cls) 49 | return Tag(name, cls) 50 | 51 | def __str__(cls): 52 | return cls.__measurement__ 53 | 54 | def __eq__(cls, other): 55 | return str(cls) == str(other) 56 | 57 | def __ne__(cls, other): 58 | return str(cls) != str(other) 59 | 60 | def __or__(cls, other): 61 | left = str(cls).strip("/") 62 | name = "_".join(left.split("|") + [str(other)]) 63 | bases = (cls,) 64 | measurement = "/%s|%s/" % (str(cls).strip("/"), other) 65 | return type(name, bases, {"__measurement__": measurement}) 66 | 67 | def __hash__(cls): 68 | return id(cls) 69 | 70 | @property 71 | def measurement(cls): 72 | """ 73 | Reflexive reference to measurement. 74 | """ 75 | return cls 76 | 77 | 78 | class Tag: 79 | """ 80 | InfluxDB Tag instance. 81 | 82 | name (str): Name of Tag 83 | measurement (Measurement): Measurement of tag 84 | """ 85 | 86 | def __init__(self, name, measurement): 87 | self._name = name 88 | self.measurement = measurement 89 | 90 | def __str__(self): 91 | return self._name 92 | 93 | def __repr__(self): 94 | return "<%s.%s>" % (self.measurement.__measurement__, self._name) 95 | 96 | def __eq__(self, other): 97 | return TagExp.equals(self, other) 98 | 99 | def __ne__(self, other): 100 | return TagExp.notequals(self, other) 101 | 102 | def __gt__(self, other): 103 | return TagExp.greater_than(self, other) 104 | 105 | def __lt__(self, other): 106 | return TagExp.less_than(self, other) 107 | 108 | def __ge__(self, other): 109 | return TagExp.greater_equal(self, other) 110 | 111 | def __le__(self, other): 112 | return TagExp.less_equal(self, other) 113 | 114 | def like(self, other): 115 | """ 116 | self =~ other 117 | """ 118 | return TagExp.like(self, other) 119 | 120 | def notlike(self, other): 121 | """ 122 | self !~ other 123 | """ 124 | return TagExp.notlike(self, other) 125 | 126 | 127 | class Time(Tag): 128 | """ 129 | Time of InfluxDB Measurement. 130 | """ 131 | 132 | def between(self, start, end, startinc=True, endinc=True): 133 | """ 134 | Query times between extremes. 135 | 136 | Arguments: 137 | start (str): Start of time 138 | end (str): End of time 139 | startinc (boolean): Start-inclusive flag 140 | endinc (boolean): End-inclusive flag 141 | 142 | Returns: 143 | Time expression. 144 | """ 145 | if startinc is True: 146 | startexp = TagExp.greater_equal(self, start) 147 | else: 148 | startexp = TagExp.greater_than(self, start) 149 | if endinc is True: 150 | endexp = TagExp.less_equal(self, end) 151 | else: 152 | endexp = TagExp.less_than(self, end) 153 | return startexp & endexp 154 | 155 | 156 | class TagExp: 157 | """ 158 | A tag query expression. 159 | """ 160 | 161 | def __init__(self, left, op, right): 162 | self._left = left 163 | self._op = op 164 | lits = [operations.LK, operations.NK, operations.AND, operations.OR] 165 | if self._op in lits or isinstance(left, Time): 166 | # If the right-hand-side value is a date/datetime object, 167 | # we convert it to RFC3339 representation 168 | # and wrap inside single quote 169 | if isinstance(right, date): 170 | right = repr(make_tz_aware(right).isoformat()) 171 | self._right = right 172 | else: 173 | self._right = repr(right) 174 | 175 | def __str__(self): 176 | return "%s%s%s" % (self._left, self._op, self._right) 177 | 178 | def __repr__(self): 179 | return "[ %s ]" % self 180 | 181 | def __eq__(self, other): 182 | return str(self) == str(other) 183 | 184 | def __ne__(self, other): 185 | return str(self) != str(other) 186 | 187 | def __and__(self, other): 188 | return TagExp(str(self), " AND ", str(other)) 189 | 190 | def __or__(self, other): 191 | return TagExp(str(self), " OR ", str(other)) 192 | 193 | def __invert__(self): 194 | return TagExp(self._left, ~self._op, self._right) 195 | 196 | @classmethod 197 | def equals(cls, self, other): 198 | """ 199 | left = right 200 | """ 201 | return cls(self, operations.EQ, other) 202 | 203 | @classmethod 204 | def notequals(cls, self, other): 205 | """ 206 | left != right 207 | """ 208 | return cls(self, operations.NE, other) 209 | 210 | @classmethod 211 | def greater_than(cls, self, other): 212 | """ 213 | left > right 214 | """ 215 | return cls(self, operations.GT, other) 216 | 217 | @classmethod 218 | def less_than(cls, self, other): 219 | """ 220 | left < right 221 | """ 222 | return cls(self, operations.LT, other) 223 | 224 | @classmethod 225 | def greater_equal(cls, self, other): 226 | """ 227 | left >= right 228 | """ 229 | return cls(self, operations.GE, other) 230 | 231 | @classmethod 232 | def less_equal(cls, self, other): 233 | """ 234 | left <= right 235 | """ 236 | return cls(self, operations.LE, other) 237 | 238 | @classmethod 239 | def like(cls, self, other): 240 | """ 241 | left =~ right 242 | """ 243 | return cls(self, operations.LK, other) 244 | 245 | @classmethod 246 | def notlike(cls, self, other): 247 | """ 248 | left !~ right 249 | """ 250 | return cls(self, operations.NK, other) 251 | -------------------------------------------------------------------------------- /notebooks/Usage.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# InfluxAlchemy" 8 | ] 9 | }, 10 | { 11 | "cell_type": "code", 12 | "execution_count": 1, 13 | "metadata": { 14 | "collapsed": false 15 | }, 16 | "outputs": [], 17 | "source": [ 18 | "import influxdb\n", 19 | "import influxalchemy" 20 | ] 21 | }, 22 | { 23 | "cell_type": "markdown", 24 | "metadata": {}, 25 | "source": [ 26 | "## Define InfluxAlchemy Measurements" 27 | ] 28 | }, 29 | { 30 | "cell_type": "code", 31 | "execution_count": 2, 32 | "metadata": { 33 | "collapsed": false 34 | }, 35 | "outputs": [], 36 | "source": [ 37 | "class Widgets(influxalchemy.Measurement):\n", 38 | " __measurement__ = 'widgets'\n", 39 | "\n", 40 | "\n", 41 | "class Wombats(influxalchemy.Measurement):\n", 42 | " __measurement__ = 'wombats'" 43 | ] 44 | }, 45 | { 46 | "cell_type": "markdown", 47 | "metadata": {}, 48 | "source": [ 49 | "## Open InfluxAlchemy Connection" 50 | ] 51 | }, 52 | { 53 | "cell_type": "code", 54 | "execution_count": 3, 55 | "metadata": { 56 | "collapsed": true 57 | }, 58 | "outputs": [], 59 | "source": [ 60 | "db = influxdb.DataFrameClient(database=\"example\")\n", 61 | "flux = influxalchemy.InfluxAlchemy(db)" 62 | ] 63 | }, 64 | { 65 | "cell_type": "markdown", 66 | "metadata": {}, 67 | "source": [ 68 | "## Query InfluxDB" 69 | ] 70 | }, 71 | { 72 | "cell_type": "markdown", 73 | "metadata": {}, 74 | "source": [ 75 | "### Query Single Measurement" 76 | ] 77 | }, 78 | { 79 | "cell_type": "code", 80 | "execution_count": 4, 81 | "metadata": { 82 | "collapsed": false 83 | }, 84 | "outputs": [ 85 | { 86 | "data": { 87 | "text/plain": [ 88 | "SELECT * FROM widgets;" 89 | ] 90 | }, 91 | "execution_count": 4, 92 | "metadata": {}, 93 | "output_type": "execute_result" 94 | } 95 | ], 96 | "source": [ 97 | "flux.query(Widgets)" 98 | ] 99 | }, 100 | { 101 | "cell_type": "markdown", 102 | "metadata": {}, 103 | "source": [ 104 | "### Query Ad Hoc Measurement" 105 | ] 106 | }, 107 | { 108 | "cell_type": "code", 109 | "execution_count": 5, 110 | "metadata": { 111 | "collapsed": false 112 | }, 113 | "outputs": [ 114 | { 115 | "data": { 116 | "text/plain": [ 117 | "SELECT * FROM /.*/;" 118 | ] 119 | }, 120 | "execution_count": 5, 121 | "metadata": {}, 122 | "output_type": "execute_result" 123 | } 124 | ], 125 | "source": [ 126 | "flux.query(influxalchemy.Measurement.new(\"/.*/\"))" 127 | ] 128 | }, 129 | { 130 | "cell_type": "markdown", 131 | "metadata": {}, 132 | "source": [ 133 | "### Select Fields of Measurement" 134 | ] 135 | }, 136 | { 137 | "cell_type": "code", 138 | "execution_count": 6, 139 | "metadata": { 140 | "collapsed": false 141 | }, 142 | "outputs": [ 143 | { 144 | "data": { 145 | "text/plain": [ 146 | "SELECT tag1, field2 FROM widgets;" 147 | ] 148 | }, 149 | "execution_count": 6, 150 | "metadata": {}, 151 | "output_type": "execute_result" 152 | } 153 | ], 154 | "source": [ 155 | "flux.query(Widgets.tag1, Widgets.field2)" 156 | ] 157 | }, 158 | { 159 | "cell_type": "markdown", 160 | "metadata": {}, 161 | "source": [ 162 | "### Query Across Measurements" 163 | ] 164 | }, 165 | { 166 | "cell_type": "code", 167 | "execution_count": 7, 168 | "metadata": { 169 | "collapsed": false 170 | }, 171 | "outputs": [ 172 | { 173 | "data": { 174 | "text/plain": [ 175 | "SELECT * FROM /widgets|wombats/;" 176 | ] 177 | }, 178 | "execution_count": 7, 179 | "metadata": {}, 180 | "output_type": "execute_result" 181 | } 182 | ], 183 | "source": [ 184 | "flux.query(Widgets | Wombats)" 185 | ] 186 | }, 187 | { 188 | "cell_type": "markdown", 189 | "metadata": {}, 190 | "source": [ 191 | "### Filter Tags" 192 | ] 193 | }, 194 | { 195 | "cell_type": "code", 196 | "execution_count": 8, 197 | "metadata": { 198 | "collapsed": false 199 | }, 200 | "outputs": [ 201 | { 202 | "data": { 203 | "text/plain": [ 204 | "SELECT * FROM widgets WHERE (tag1 = 'fizz');" 205 | ] 206 | }, 207 | "execution_count": 8, 208 | "metadata": {}, 209 | "output_type": "execute_result" 210 | } 211 | ], 212 | "source": [ 213 | "flux.query(Widgets).filter(Widgets.tag1 == \"fizz\")" 214 | ] 215 | }, 216 | { 217 | "cell_type": "markdown", 218 | "metadata": {}, 219 | "source": [ 220 | "### Filter Tags with 'like'" 221 | ] 222 | }, 223 | { 224 | "cell_type": "code", 225 | "execution_count": 9, 226 | "metadata": { 227 | "collapsed": false 228 | }, 229 | "outputs": [ 230 | { 231 | "data": { 232 | "text/plain": [ 233 | "SELECT * FROM widgets WHERE (tag1 =~ /z$/);" 234 | ] 235 | }, 236 | "execution_count": 9, 237 | "metadata": {}, 238 | "output_type": "execute_result" 239 | } 240 | ], 241 | "source": [ 242 | "flux.query(Widgets).filter(Widgets.tag1.like(\"/z$/\"))" 243 | ] 244 | }, 245 | { 246 | "cell_type": "markdown", 247 | "metadata": {}, 248 | "source": [ 249 | "### Chain Filters" 250 | ] 251 | }, 252 | { 253 | "cell_type": "code", 254 | "execution_count": 10, 255 | "metadata": { 256 | "collapsed": false 257 | }, 258 | "outputs": [ 259 | { 260 | "data": { 261 | "text/plain": [ 262 | "SELECT * FROM widgets WHERE (tag1 = 'fizz' AND tag2 = 'buzz');" 263 | ] 264 | }, 265 | "execution_count": 10, 266 | "metadata": {}, 267 | "output_type": "execute_result" 268 | } 269 | ], 270 | "source": [ 271 | "clause1 = Widgets.tag1 == \"fizz\"\n", 272 | "clause2 = Widgets.tag2 == \"buzz\"\n", 273 | "flux.query(Widgets).filter(clause1 & clause2)" 274 | ] 275 | }, 276 | { 277 | "cell_type": "code", 278 | "execution_count": 11, 279 | "metadata": { 280 | "collapsed": false 281 | }, 282 | "outputs": [ 283 | { 284 | "data": { 285 | "text/plain": [ 286 | "SELECT * FROM widgets WHERE (tag1 = 'fizz' OR tag2 = 'buzz');" 287 | ] 288 | }, 289 | "execution_count": 11, 290 | "metadata": {}, 291 | "output_type": "execute_result" 292 | } 293 | ], 294 | "source": [ 295 | "flux.query(Widgets).filter(clause1 | clause2)" 296 | ] 297 | }, 298 | { 299 | "cell_type": "markdown", 300 | "metadata": { 301 | "collapsed": true 302 | }, 303 | "source": [ 304 | "### Group Bys" 305 | ] 306 | }, 307 | { 308 | "cell_type": "code", 309 | "execution_count": 12, 310 | "metadata": { 311 | "collapsed": false 312 | }, 313 | "outputs": [ 314 | { 315 | "data": { 316 | "text/plain": [ 317 | "SELECT * FROM widgets GROUP BY time(1d);" 318 | ] 319 | }, 320 | "execution_count": 12, 321 | "metadata": {}, 322 | "output_type": "execute_result" 323 | } 324 | ], 325 | "source": [ 326 | "flux.query(Widgets).group_by(\"time(1d)\")" 327 | ] 328 | }, 329 | { 330 | "cell_type": "code", 331 | "execution_count": 13, 332 | "metadata": { 333 | "collapsed": false 334 | }, 335 | "outputs": [ 336 | { 337 | "data": { 338 | "text/plain": [ 339 | "SELECT * FROM widgets GROUP BY tag1;" 340 | ] 341 | }, 342 | "execution_count": 13, 343 | "metadata": {}, 344 | "output_type": "execute_result" 345 | } 346 | ], 347 | "source": [ 348 | "flux.query(Widgets).group_by(Widgets.tag1)" 349 | ] 350 | }, 351 | { 352 | "cell_type": "markdown", 353 | "metadata": {}, 354 | "source": [ 355 | "### Time" 356 | ] 357 | }, 358 | { 359 | "cell_type": "code", 360 | "execution_count": 14, 361 | "metadata": { 362 | "collapsed": false 363 | }, 364 | "outputs": [ 365 | { 366 | "data": { 367 | "text/plain": [ 368 | "SELECT * FROM widgets WHERE (time > now() - 7d);" 369 | ] 370 | }, 371 | "execution_count": 14, 372 | "metadata": {}, 373 | "output_type": "execute_result" 374 | } 375 | ], 376 | "source": [ 377 | "flux.query(Widgets).filter(Widgets.time > \"now() - 7d\")" 378 | ] 379 | }, 380 | { 381 | "cell_type": "code", 382 | "execution_count": 15, 383 | "metadata": { 384 | "collapsed": false 385 | }, 386 | "outputs": [ 387 | { 388 | "data": { 389 | "text/plain": [ 390 | "SELECT * FROM widgets WHERE (time >= '2016-01-01' AND time <= now() - 7d);" 391 | ] 392 | }, 393 | "execution_count": 15, 394 | "metadata": {}, 395 | "output_type": "execute_result" 396 | } 397 | ], 398 | "source": [ 399 | "flux.query(Widgets).filter(Widgets.time.between(\"'2016-01-01'\", \"now() - 7d\"))" 400 | ] 401 | }, 402 | { 403 | "cell_type": "code", 404 | "execution_count": null, 405 | "metadata": { 406 | "collapsed": true 407 | }, 408 | "outputs": [], 409 | "source": [] 410 | } 411 | ], 412 | "metadata": { 413 | "kernelspec": { 414 | "display_name": "Python 2", 415 | "language": "python", 416 | "name": "python2" 417 | }, 418 | "language_info": { 419 | "codemirror_mode": { 420 | "name": "ipython", 421 | "version": 2 422 | }, 423 | "file_extension": ".py", 424 | "mimetype": "text/x-python", 425 | "name": "python", 426 | "nbconvert_exporter": "python", 427 | "pygments_lexer": "ipython2", 428 | "version": "2.7.12" 429 | } 430 | }, 431 | "nbformat": 4, 432 | "nbformat_minor": 0 433 | } 434 | --------------------------------------------------------------------------------