clickhouse-orm 3.0.1__py2.py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,170 @@
1
+ """
2
+ This file contains system readonly models that can be got from the database
3
+ https://clickhouse.tech/docs/en/system_tables/
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from .database import Database
9
+ from .fields import DateTimeField, StringField, UInt8Field, UInt32Field, UInt64Field
10
+ from .models import Model
11
+ from .utils import comma_join
12
+
13
+
14
+ class SystemPart(Model):
15
+ """
16
+ Contains information about parts of a table in the MergeTree family.
17
+ This model operates only fields, described in the reference. Other fields are ignored.
18
+ https://clickhouse.tech/docs/en/system_tables/system.parts/
19
+ """
20
+
21
+ OPERATIONS = frozenset({"DETACH", "DROP", "ATTACH", "FREEZE", "FETCH"})
22
+
23
+ _readonly = True
24
+ _system = True
25
+
26
+ database = StringField() # Name of the database where the table that this part belongs to is located.
27
+ table = StringField() # Name of the table that this part belongs to.
28
+ engine = StringField() # Name of the table engine, without parameters.
29
+ partition = StringField() # Name of the partition, in the format YYYYMM.
30
+ name = StringField() # Name of the part.
31
+
32
+ # This field is present in the docs (https://clickhouse.tech/docs/en/single/index.html#system-parts),
33
+ # but is absent in ClickHouse (in version 1.1.54245)
34
+ # replicated = UInt8Field() # Whether the part belongs to replicated data.
35
+
36
+ # Whether the part is used in a table, or is no longer needed and will be deleted soon.
37
+ # Inactive parts remain after merging.
38
+ active = UInt8Field()
39
+
40
+ # Number of marks - multiply by the index granularity (usually 8192)
41
+ # to get the approximate number of rows in the part.
42
+ marks = UInt64Field()
43
+
44
+ bytes = UInt64Field() # Number of bytes when compressed.
45
+
46
+ # Time the directory with the part was modified. Usually corresponds to the part's creation time.
47
+ modification_time = DateTimeField()
48
+ remove_time = DateTimeField() # For inactive parts only - the time when the part became inactive.
49
+
50
+ # The number of places where the part is used. A value greater than 2 indicates
51
+ # that this part participates in queries or merges.
52
+ refcount = UInt32Field()
53
+
54
+ @classmethod
55
+ def table_name(cls):
56
+ return "parts"
57
+
58
+ """
59
+ Next methods return SQL for some operations, which can be done with partitions
60
+ https://clickhouse.tech/docs/en/query_language/queries/#manipulations-with-partitions-and-parts
61
+ """
62
+
63
+ def _partition_operation_sql(self, operation, settings=None, from_part=None):
64
+ """
65
+ Performs some operation over partition
66
+
67
+ - `db`: Database object to execute operation on
68
+ - `operation`: Operation to execute from SystemPart.OPERATIONS set
69
+ - `settings`: Settings for executing request to ClickHouse over db.raw() method
70
+
71
+ Returns: Operation execution result
72
+ """
73
+ operation = operation.upper()
74
+ assert operation in self.OPERATIONS, "operation must be in [%s]" % comma_join(self.OPERATIONS)
75
+
76
+ sql = "ALTER TABLE `%s`.`%s` %s PARTITION %s" % (self._database.db_name, self.table, operation, self.partition)
77
+ if from_part is not None:
78
+ sql += " FROM %s" % from_part
79
+ self._database.raw(sql, settings=settings, stream=False)
80
+
81
+ def detach(self, settings=None):
82
+ """
83
+ Move a partition to the 'detached' directory and forget it.
84
+
85
+ - `settings`: Settings for executing request to ClickHouse over db.raw() method
86
+
87
+ Returns: SQL Query
88
+ """
89
+ return self._partition_operation_sql("DETACH", settings=settings)
90
+
91
+ def drop(self, settings=None):
92
+ """
93
+ Delete a partition
94
+
95
+ - `settings`: Settings for executing request to ClickHouse over db.raw() method
96
+
97
+ Returns: SQL Query
98
+ """
99
+ return self._partition_operation_sql("DROP", settings=settings)
100
+
101
+ def attach(self, settings=None):
102
+ """
103
+ Add a new part or partition from the 'detached' directory to the table.
104
+
105
+ - `settings`: Settings for executing request to ClickHouse over db.raw() method
106
+
107
+ Returns: SQL Query
108
+ """
109
+ return self._partition_operation_sql("ATTACH", settings=settings)
110
+
111
+ def freeze(self, settings=None):
112
+ """
113
+ Create a backup of a partition.
114
+
115
+ - `settings`: Settings for executing request to ClickHouse over db.raw() method
116
+
117
+ Returns: SQL Query
118
+ """
119
+ return self._partition_operation_sql("FREEZE", settings=settings)
120
+
121
+ def fetch(self, zookeeper_path, settings=None):
122
+ """
123
+ Download a partition from another server.
124
+
125
+ - `zookeeper_path`: Path in zookeeper to fetch from
126
+ - `settings`: Settings for executing request to ClickHouse over db.raw() method
127
+
128
+ Returns: SQL Query
129
+ """
130
+ return self._partition_operation_sql("FETCH", settings=settings, from_part=zookeeper_path)
131
+
132
+ @classmethod
133
+ def get(cls, database, conditions=""):
134
+ """
135
+ Get all data from system.parts table
136
+
137
+ - `database`: A database object to fetch data from.
138
+ - `conditions`: WHERE clause conditions. Database condition is added automatically
139
+
140
+ Returns: A list of SystemPart objects
141
+ """
142
+ assert isinstance(database, Database), "database must be database.Database class instance"
143
+ assert isinstance(conditions, str), "conditions must be a string"
144
+ if conditions:
145
+ conditions += " AND"
146
+ field_names = ",".join(cls.fields())
147
+ return database.select(
148
+ "SELECT %s FROM `system`.%s WHERE %s database='%s'"
149
+ % (field_names, cls.table_name(), conditions, database.db_name),
150
+ model_class=cls,
151
+ )
152
+
153
+ @classmethod
154
+ def get_active(cls, database, conditions=""):
155
+ """
156
+ Gets active data from system.parts table
157
+
158
+ - `database`: A database object to fetch data from.
159
+ - `conditions`: WHERE clause conditions. Database and active conditions are added automatically
160
+
161
+ Returns: A list of SystemPart objects
162
+ """
163
+ if conditions:
164
+ conditions += " AND "
165
+ conditions += "active"
166
+ return SystemPart.get(database, conditions=conditions)
167
+
168
+
169
+ # Expose only relevant classes in import *
170
+ __all__ = [c.__name__ for c in [SystemPart]]
@@ -0,0 +1,176 @@
1
+ from __future__ import annotations
2
+
3
+ import codecs
4
+ import importlib
5
+ import pkgutil
6
+ import re
7
+ from datetime import date, datetime, timedelta, tzinfo
8
+ from inspect import isclass
9
+ from typing import TYPE_CHECKING, NamedTuple
10
+
11
+ if TYPE_CHECKING:
12
+ from collections.abc import Iterable
13
+ from types import ModuleType
14
+ from typing import Any
15
+
16
+
17
+ class Page(NamedTuple):
18
+ """A simple data structure for paginated results."""
19
+
20
+ objects: list[Any]
21
+ number_of_objects: int
22
+ pages_total: int
23
+ number: int
24
+ page_size: int
25
+
26
+
27
+ def escape(value: str, quote: bool = True) -> str:
28
+ """
29
+ If the value is a string, escapes any special characters and optionally
30
+ surrounds it with single quotes. If the value is not a string (e.g. a number),
31
+ converts it to one.
32
+ """
33
+ value = codecs.escape_encode(value.encode("utf-8"))[0].decode("utf-8")
34
+ if quote:
35
+ value = "'" + value + "'"
36
+
37
+ return value
38
+
39
+
40
+ def unescape(value: str) -> str | None:
41
+ if value == "\\N":
42
+ return None
43
+ return codecs.escape_decode(value)[0].decode("utf-8")
44
+
45
+
46
+ def string_or_func(obj):
47
+ return obj.to_sql() if hasattr(obj, "to_sql") else obj
48
+
49
+
50
+ def arg_to_sql(arg: Any) -> str:
51
+ """
52
+ Converts a function argument to SQL string according to its type.
53
+ Supports functions, model fields, strings, dates, datetimes, timedeltas, booleans,
54
+ None, numbers, timezones, arrays/iterables.
55
+ """
56
+ from clickhouse_orm import DateTimeField, F, Field, QuerySet, StringField
57
+
58
+ if isinstance(arg, F):
59
+ return arg.to_sql()
60
+ if isinstance(arg, Field):
61
+ return "`%s`" % arg
62
+ if isinstance(arg, str):
63
+ return StringField().to_db_string(arg)
64
+ if isinstance(arg, datetime):
65
+ return "toDateTime(%s)" % DateTimeField().to_db_string(arg)
66
+ if isinstance(arg, date):
67
+ return "toDate('%s')" % arg.isoformat()
68
+ if isinstance(arg, timedelta):
69
+ return "toIntervalSecond(%d)" % int(arg.total_seconds())
70
+ if isinstance(arg, bool):
71
+ return str(int(arg))
72
+ if isinstance(arg, tzinfo):
73
+ return StringField().to_db_string(arg.tzname(None))
74
+ if arg is None:
75
+ return "NULL"
76
+ if isinstance(arg, QuerySet):
77
+ return "(%s)" % arg
78
+ if isinstance(arg, tuple):
79
+ return "(" + comma_join(arg_to_sql(x) for x in arg) + ")"
80
+ if is_iterable(arg):
81
+ return "[" + comma_join(arg_to_sql(x) for x in arg) + "]"
82
+ return str(arg)
83
+
84
+
85
+ def parse_tsv(line: bytes | str) -> list[str]:
86
+ if isinstance(line, bytes):
87
+ line = line.decode()
88
+ if line and line[-1] == "\n":
89
+ line = line[:-1]
90
+ return [unescape(value) for value in line.split("\t")]
91
+
92
+
93
+ def parse_array(array_string: str) -> list[Any]:
94
+ """
95
+ Parse an array or tuple string as returned by clickhouse. For example:
96
+ "['hello', 'world']" ==> ["hello", "world"]
97
+ "(1,2,3)" ==> [1, 2, 3]
98
+ """
99
+ # Sanity check
100
+ if len(array_string) < 2 or array_string[0] not in "[(" or array_string[-1] not in "])":
101
+ raise ValueError('Invalid array string: "%s"' % array_string)
102
+ # Drop opening brace
103
+ array_string = array_string[1:]
104
+ # Go over the string, lopping off each value at the beginning until nothing is left
105
+ values = []
106
+ while True:
107
+ if array_string in "])":
108
+ # End of array
109
+ return values
110
+ elif array_string[0] in ", ":
111
+ # In between values
112
+ array_string = array_string[1:]
113
+ elif array_string[0] == "'":
114
+ # Start of quoted value, find its end
115
+ match = re.search(r"[^\\]'", array_string)
116
+ if match is None:
117
+ raise ValueError('Missing closing quote: "%s"' % array_string)
118
+ values.append(array_string[1 : match.start() + 1])
119
+ array_string = array_string[match.end() :]
120
+ else:
121
+ # Start of non-quoted value, find its end
122
+ match = re.search(r",|\]", array_string)
123
+ values.append(array_string[0 : match.start()])
124
+ array_string = array_string[match.end() - 1 :]
125
+
126
+
127
+ def import_submodules(package_name: str) -> dict[str, ModuleType]:
128
+ """
129
+ Import all submodules of a module.
130
+ """
131
+ package = importlib.import_module(package_name)
132
+ return {
133
+ name: importlib.import_module(package_name + "." + name)
134
+ for _, name, _ in pkgutil.iter_modules(package.__path__)
135
+ }
136
+
137
+
138
+ def comma_join(items: Iterable[str]) -> str:
139
+ """
140
+ Joins an iterable of strings with commas.
141
+ """
142
+ return ", ".join(items)
143
+
144
+
145
+ def is_iterable(obj: Any) -> bool:
146
+ """
147
+ Checks if the given object is iterable.
148
+ """
149
+ try:
150
+ iter(obj)
151
+ return True
152
+ except TypeError:
153
+ return False
154
+
155
+
156
+ def get_subclass_names(locals: dict[str, Any], base_class: type):
157
+ return [c.__name__ for c in locals.values() if isclass(c) and issubclass(c, base_class)]
158
+
159
+
160
+ class NoValue:
161
+ """
162
+ A sentinel for fields with an expression for a default value,
163
+ that were not assigned a value yet.
164
+ """
165
+
166
+ def __repr__(self):
167
+ return "NO_VALUE"
168
+
169
+ def __copy__(self):
170
+ return self
171
+
172
+ def __deepcopy__(self, memo):
173
+ return self
174
+
175
+
176
+ NO_VALUE = NoValue()
@@ -0,0 +1,90 @@
1
+ Metadata-Version: 2.4
2
+ Name: clickhouse_orm
3
+ Version: 3.0.1
4
+ Summary: A simple ORM for working with the Clickhouse database. Maintainance fork of infi.clickhouse_orm.
5
+ Author-email: Oliver Margetts <oliver.margetts@gmail.com>
6
+ Description-Content-Type: text/markdown
7
+ Classifier: Intended Audience :: Developers
8
+ Classifier: Intended Audience :: System Administrators
9
+ Classifier: License :: OSI Approved :: BSD License
10
+ Classifier: Operating System :: OS Independent
11
+ Classifier: Programming Language :: Python
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Classifier: Programming Language :: Python :: 3.14
16
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
17
+ Classifier: Topic :: Database
18
+ License-File: LICENSE
19
+ Requires-Dist: requests
20
+ Requires-Dist: pytz
21
+ Requires-Dist: docker==7.1.0 ; extra == "dev"
22
+ Requires-Dist: pytest==9.0.2 ; extra == "dev"
23
+ Requires-Dist: ruff==0.14.14 ; extra == "dev"
24
+ Project-URL: Homepage, https://github.com/SuadeLabs/clickhouse_orm
25
+ Project-URL: Repository, https://github.com/SuadeLabs/clickhouse_orm
26
+ Provides-Extra: dev
27
+
28
+ A fork of [infi.clikchouse_orm](https://github.com/Infinidat/infi.clickhouse_orm) aimed at more frequent maintenance and bugfixes.
29
+
30
+ [![Tests](https://github.com/SuadeLabs/clickhouse_orm/actions/workflows/python-test.yml/badge.svg)](https://github.com/SuadeLabs/clickhouse_orm/actions/workflows/python-test.yml)
31
+ ![PyPI](https://img.shields.io/pypi/v/clickhouse_orm)
32
+
33
+ Introduction
34
+ ============
35
+
36
+ This project is simple ORM for working with the [ClickHouse database](https://clickhouse.yandex/).
37
+ It allows you to define model classes whose instances can be written to the database and read from it.
38
+
39
+ Let's jump right in with a simple example of monitoring CPU usage. First we need to define the model class,
40
+ connect to the database and create a table for the model:
41
+
42
+ ```python
43
+ from clickhouse_orm import Database, Model, DateTimeField, UInt16Field, Float32Field, Memory, F
44
+
45
+ class CPUStats(Model):
46
+
47
+ timestamp = DateTimeField()
48
+ cpu_id = UInt16Field()
49
+ cpu_percent = Float32Field()
50
+
51
+ engine = Memory()
52
+
53
+ db = Database('demo')
54
+ db.create_table(CPUStats)
55
+ ```
56
+
57
+ Now we can collect usage statistics per CPU, and write them to the database:
58
+
59
+ ```python
60
+ import psutil, time, datetime
61
+
62
+ psutil.cpu_percent(percpu=True) # first sample should be discarded
63
+ while True:
64
+ time.sleep(1)
65
+ stats = psutil.cpu_percent(percpu=True)
66
+ timestamp = datetime.datetime.now()
67
+ db.insert([
68
+ CPUStats(timestamp=timestamp, cpu_id=cpu_id, cpu_percent=cpu_percent)
69
+ for cpu_id, cpu_percent in enumerate(stats)
70
+ ])
71
+ ```
72
+
73
+ Querying the table is easy, using either the query builder or raw SQL:
74
+
75
+ ```python
76
+ # Calculate what percentage of the time CPU 1 was over 95% busy
77
+ queryset = CPUStats.objects_in(db)
78
+ total = queryset.filter(CPUStats.cpu_id == 1).count()
79
+ busy = queryset.filter(CPUStats.cpu_id == 1, CPUStats.cpu_percent > 95).count()
80
+ print('CPU 1 was busy {:.2f}% of the time'.format(busy * 100.0 / total))
81
+
82
+ # Calculate the average usage per CPU
83
+ for row in queryset.aggregate(CPUStats.cpu_id, average=F.avg(CPUStats.cpu_percent)):
84
+ print('CPU {row.cpu_id}: {row.average:.2f}%'.format(row=row))
85
+ ```
86
+
87
+ This and other examples can be found in the `examples` folder.
88
+
89
+ To learn more please visit the [documentation](docs/toc.md).
90
+
@@ -0,0 +1,14 @@
1
+ clickhouse_orm/__init__.py,sha256=ksYW9scTnD6VVCj8Rzv7B41VkvHDn-JWpyIh1Tjserc,478
2
+ clickhouse_orm/database.py,sha256=v4I2NKxkwF6Bm2hJAM6vCg-n9ss10THA9AIoxuHnU1g,17828
3
+ clickhouse_orm/engines.py,sha256=AV80AmLRM3hlEXj3o6GPjjKDPk8YyRdTEns9SZCppA0,11166
4
+ clickhouse_orm/fields.py,sha256=Wfm3NkctRhnaw2bc9KqlncV04FCvBEb_74ky9ZF2aq4,24039
5
+ clickhouse_orm/funcs.py,sha256=8S1kdwCjIJ45imvA2XkxHjDhGOYlNg4aS1vMU2CQ224,42108
6
+ clickhouse_orm/migrations.py,sha256=_AmD6uQQv3CEm0wc7JWawMcYsKQRId5nDFXr4IT-J6Y,10470
7
+ clickhouse_orm/models.py,sha256=pUkLBClnfyXPg5xuHTpobNhFqHtKuHlXTNwWv5PnV_U,23088
8
+ clickhouse_orm/query.py,sha256=3-v1Z5G1xO1c8ywVhmZiz0tpAvBmH19xzeyxVpxc6uU,24103
9
+ clickhouse_orm/system_models.py,sha256=pU-Pvq69GH6QD4iKIrSRxw3Q6n-qlgQzGLqpYwAF12U,6447
10
+ clickhouse_orm/utils.py,sha256=2fEmTrBSDNpPzbbnUOZeCQ2NX5httkOKrJKzgdTezH4,5179
11
+ clickhouse_orm-3.0.1.dist-info/licenses/LICENSE,sha256=MP0KDNu_tFQJrr35YkSFnwwp2F8p2a7Q1Mi2lOs8FLw,1503
12
+ clickhouse_orm-3.0.1.dist-info/WHEEL,sha256=Dyt6SBfaasWElUrURkknVFAZDHSTwxg3PaTza7RSbkY,100
13
+ clickhouse_orm-3.0.1.dist-info/METADATA,sha256=ttRTc2omK1LNWMgA4FDcj02yuKom3wujU4A0dFsxkz8,3400
14
+ clickhouse_orm-3.0.1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: flit 3.12.0
3
+ Root-Is-Purelib: true
4
+ Tag: py2-none-any
5
+ Tag: py3-none-any
@@ -0,0 +1,27 @@
1
+ Copyright (c) 2021 Suade Labs
2
+ Copyright (c) 2017 INFINIDAT
3
+
4
+ Redistribution and use in source and binary forms, with or without
5
+ modification, are permitted provided that the following conditions are met:
6
+
7
+ 1. Redistributions of source code must retain the above copyright notice, this
8
+ list of conditions and the following disclaimer.
9
+
10
+ 2. Redistributions in binary form must reproduce the above copyright notice,
11
+ this list of conditions and the following disclaimer in the documentation
12
+ and/or other materials provided with the distribution.
13
+
14
+ 3. Neither the name of the copyright holder nor the names of its contributors
15
+ may be used to endorse or promote products derived from this software
16
+ without specific prior written permission.
17
+
18
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
19
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
20
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
22
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
23
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
24
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
25
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
26
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
27
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.