iceaxe 0.2.3.dev2__tar.gz → 0.2.3.dev3__tar.gz
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.
- {iceaxe-0.2.3.dev2 → iceaxe-0.2.3.dev3}/PKG-INFO +1 -1
- {iceaxe-0.2.3.dev2 → iceaxe-0.2.3.dev3}/iceaxe/__tests__/test_queries.py +10 -0
- {iceaxe-0.2.3.dev2 → iceaxe-0.2.3.dev3}/iceaxe/__tests__/test_session.py +26 -0
- {iceaxe-0.2.3.dev2 → iceaxe-0.2.3.dev3}/iceaxe/queries.py +30 -0
- {iceaxe-0.2.3.dev2 → iceaxe-0.2.3.dev3}/iceaxe/session_optimized.pyx +21 -10
- {iceaxe-0.2.3.dev2 → iceaxe-0.2.3.dev3}/pyproject.toml +1 -1
- {iceaxe-0.2.3.dev2 → iceaxe-0.2.3.dev3}/setup.py +1 -1
- {iceaxe-0.2.3.dev2 → iceaxe-0.2.3.dev3}/LICENSE +0 -0
- {iceaxe-0.2.3.dev2 → iceaxe-0.2.3.dev3}/README.md +0 -0
- {iceaxe-0.2.3.dev2 → iceaxe-0.2.3.dev3}/build.py +0 -0
- {iceaxe-0.2.3.dev2 → iceaxe-0.2.3.dev3}/iceaxe/.DS_Store +0 -0
- {iceaxe-0.2.3.dev2 → iceaxe-0.2.3.dev3}/iceaxe/__init__.py +0 -0
- {iceaxe-0.2.3.dev2 → iceaxe-0.2.3.dev3}/iceaxe/__tests__/__init__.py +0 -0
- {iceaxe-0.2.3.dev2 → iceaxe-0.2.3.dev3}/iceaxe/__tests__/benchmarks/__init__.py +0 -0
- {iceaxe-0.2.3.dev2 → iceaxe-0.2.3.dev3}/iceaxe/__tests__/benchmarks/test_select.py +0 -0
- {iceaxe-0.2.3.dev2 → iceaxe-0.2.3.dev3}/iceaxe/__tests__/conf_models.py +0 -0
- {iceaxe-0.2.3.dev2 → iceaxe-0.2.3.dev3}/iceaxe/__tests__/conftest.py +0 -0
- {iceaxe-0.2.3.dev2 → iceaxe-0.2.3.dev3}/iceaxe/__tests__/migrations/__init__.py +0 -0
- {iceaxe-0.2.3.dev2 → iceaxe-0.2.3.dev3}/iceaxe/__tests__/migrations/conftest.py +0 -0
- {iceaxe-0.2.3.dev2 → iceaxe-0.2.3.dev3}/iceaxe/__tests__/migrations/test_action_sorter.py +0 -0
- {iceaxe-0.2.3.dev2 → iceaxe-0.2.3.dev3}/iceaxe/__tests__/migrations/test_generator.py +0 -0
- {iceaxe-0.2.3.dev2 → iceaxe-0.2.3.dev3}/iceaxe/__tests__/migrations/test_generics.py +0 -0
- {iceaxe-0.2.3.dev2 → iceaxe-0.2.3.dev3}/iceaxe/__tests__/mountaineer/__init__.py +0 -0
- {iceaxe-0.2.3.dev2 → iceaxe-0.2.3.dev3}/iceaxe/__tests__/mountaineer/dependencies/__init__.py +0 -0
- {iceaxe-0.2.3.dev2 → iceaxe-0.2.3.dev3}/iceaxe/__tests__/mountaineer/dependencies/test_core.py +0 -0
- {iceaxe-0.2.3.dev2 → iceaxe-0.2.3.dev3}/iceaxe/__tests__/schemas/__init__.py +0 -0
- {iceaxe-0.2.3.dev2 → iceaxe-0.2.3.dev3}/iceaxe/__tests__/schemas/test_actions.py +0 -0
- {iceaxe-0.2.3.dev2 → iceaxe-0.2.3.dev3}/iceaxe/__tests__/schemas/test_cli.py +0 -0
- {iceaxe-0.2.3.dev2 → iceaxe-0.2.3.dev3}/iceaxe/__tests__/schemas/test_db_memory_serializer.py +0 -0
- {iceaxe-0.2.3.dev2 → iceaxe-0.2.3.dev3}/iceaxe/__tests__/schemas/test_db_serializer.py +0 -0
- {iceaxe-0.2.3.dev2 → iceaxe-0.2.3.dev3}/iceaxe/__tests__/schemas/test_db_stubs.py +0 -0
- {iceaxe-0.2.3.dev2 → iceaxe-0.2.3.dev3}/iceaxe/__tests__/test_base.py +0 -0
- {iceaxe-0.2.3.dev2 → iceaxe-0.2.3.dev3}/iceaxe/__tests__/test_comparison.py +0 -0
- {iceaxe-0.2.3.dev2 → iceaxe-0.2.3.dev3}/iceaxe/__tests__/test_field.py +0 -0
- {iceaxe-0.2.3.dev2 → iceaxe-0.2.3.dev3}/iceaxe/base.py +0 -0
- {iceaxe-0.2.3.dev2 → iceaxe-0.2.3.dev3}/iceaxe/comparison.py +0 -0
- {iceaxe-0.2.3.dev2 → iceaxe-0.2.3.dev3}/iceaxe/field.py +0 -0
- {iceaxe-0.2.3.dev2 → iceaxe-0.2.3.dev3}/iceaxe/functions.py +0 -0
- {iceaxe-0.2.3.dev2 → iceaxe-0.2.3.dev3}/iceaxe/generics.py +0 -0
- {iceaxe-0.2.3.dev2 → iceaxe-0.2.3.dev3}/iceaxe/io.py +0 -0
- {iceaxe-0.2.3.dev2 → iceaxe-0.2.3.dev3}/iceaxe/logging.py +0 -0
- {iceaxe-0.2.3.dev2 → iceaxe-0.2.3.dev3}/iceaxe/migrations/__init__.py +0 -0
- {iceaxe-0.2.3.dev2 → iceaxe-0.2.3.dev3}/iceaxe/migrations/action_sorter.py +0 -0
- {iceaxe-0.2.3.dev2 → iceaxe-0.2.3.dev3}/iceaxe/migrations/cli.py +0 -0
- {iceaxe-0.2.3.dev2 → iceaxe-0.2.3.dev3}/iceaxe/migrations/client_io.py +0 -0
- {iceaxe-0.2.3.dev2 → iceaxe-0.2.3.dev3}/iceaxe/migrations/generator.py +0 -0
- {iceaxe-0.2.3.dev2 → iceaxe-0.2.3.dev3}/iceaxe/migrations/migration.py +0 -0
- {iceaxe-0.2.3.dev2 → iceaxe-0.2.3.dev3}/iceaxe/migrations/migrator.py +0 -0
- {iceaxe-0.2.3.dev2 → iceaxe-0.2.3.dev3}/iceaxe/mountaineer/__init__.py +0 -0
- {iceaxe-0.2.3.dev2 → iceaxe-0.2.3.dev3}/iceaxe/mountaineer/cli.py +0 -0
- {iceaxe-0.2.3.dev2 → iceaxe-0.2.3.dev3}/iceaxe/mountaineer/config.py +0 -0
- {iceaxe-0.2.3.dev2 → iceaxe-0.2.3.dev3}/iceaxe/mountaineer/dependencies/__init__.py +0 -0
- {iceaxe-0.2.3.dev2 → iceaxe-0.2.3.dev3}/iceaxe/mountaineer/dependencies/core.py +0 -0
- {iceaxe-0.2.3.dev2 → iceaxe-0.2.3.dev3}/iceaxe/postgres.py +0 -0
- {iceaxe-0.2.3.dev2 → iceaxe-0.2.3.dev3}/iceaxe/py.typed +0 -0
- {iceaxe-0.2.3.dev2 → iceaxe-0.2.3.dev3}/iceaxe/queries_str.py +0 -0
- {iceaxe-0.2.3.dev2 → iceaxe-0.2.3.dev3}/iceaxe/schemas/__init__.py +0 -0
- {iceaxe-0.2.3.dev2 → iceaxe-0.2.3.dev3}/iceaxe/schemas/actions.py +0 -0
- {iceaxe-0.2.3.dev2 → iceaxe-0.2.3.dev3}/iceaxe/schemas/cli.py +0 -0
- {iceaxe-0.2.3.dev2 → iceaxe-0.2.3.dev3}/iceaxe/schemas/db_memory_serializer.py +0 -0
- {iceaxe-0.2.3.dev2 → iceaxe-0.2.3.dev3}/iceaxe/schemas/db_serializer.py +0 -0
- {iceaxe-0.2.3.dev2 → iceaxe-0.2.3.dev3}/iceaxe/schemas/db_stubs.py +0 -0
- {iceaxe-0.2.3.dev2 → iceaxe-0.2.3.dev3}/iceaxe/session.py +0 -0
- {iceaxe-0.2.3.dev2 → iceaxe-0.2.3.dev3}/iceaxe/typing.py +0 -0
|
@@ -267,3 +267,13 @@ def test_select_multiple_typehints():
|
|
|
267
267
|
query = select((UserDemo, UserDemo.id, UserDemo.name))
|
|
268
268
|
if TYPE_CHECKING:
|
|
269
269
|
_: QueryBuilder[tuple[UserDemo, int, str], Literal["SELECT"]] = query
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def test_allow_branching():
|
|
273
|
+
base_query = select(UserDemo)
|
|
274
|
+
|
|
275
|
+
query_1 = base_query.limit(1)
|
|
276
|
+
query_2 = base_query.limit(2)
|
|
277
|
+
|
|
278
|
+
assert query_1.limit_value == 1
|
|
279
|
+
assert query_2.limit_value == 2
|
|
@@ -442,6 +442,32 @@ async def test_select_with_left_join(db_connection: DBConnection):
|
|
|
442
442
|
assert result[1] == ("John", 2)
|
|
443
443
|
|
|
444
444
|
|
|
445
|
+
@pytest.mark.asyncio
|
|
446
|
+
async def test_select_with_left_join_object(db_connection: DBConnection):
|
|
447
|
+
users = [
|
|
448
|
+
UserDemo(name="John", email="john@example.com"),
|
|
449
|
+
UserDemo(name="Jane", email="jane@example.com"),
|
|
450
|
+
]
|
|
451
|
+
await db_connection.insert(users)
|
|
452
|
+
|
|
453
|
+
posts = [
|
|
454
|
+
ArtifactDemo(title="John's Post", user_id=users[0].id),
|
|
455
|
+
ArtifactDemo(title="Another Post", user_id=users[0].id),
|
|
456
|
+
]
|
|
457
|
+
await db_connection.insert(posts)
|
|
458
|
+
|
|
459
|
+
query = (
|
|
460
|
+
QueryBuilder()
|
|
461
|
+
.select((UserDemo, ArtifactDemo))
|
|
462
|
+
.join(ArtifactDemo, UserDemo.id == ArtifactDemo.user_id, "LEFT")
|
|
463
|
+
)
|
|
464
|
+
result = await db_connection.exec(query)
|
|
465
|
+
assert len(result) == 3
|
|
466
|
+
assert result[0] == (users[0], posts[0])
|
|
467
|
+
assert result[1] == (users[0], posts[1])
|
|
468
|
+
assert result[2] == (users[1], None)
|
|
469
|
+
|
|
470
|
+
|
|
445
471
|
# @pytest.mark.asyncio
|
|
446
472
|
# async def test_select_with_subquery(db_connection: DBConnection):
|
|
447
473
|
# users = [
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
from copy import copy
|
|
4
|
+
from functools import wraps
|
|
3
5
|
from typing import Any, Generic, Literal, Type, TypeVar, TypeVarTuple, cast, overload
|
|
4
6
|
|
|
5
7
|
from iceaxe.base import (
|
|
@@ -53,6 +55,22 @@ JoinType = Literal["INNER", "LEFT", "RIGHT", "FULL"]
|
|
|
53
55
|
OrderDirection = Literal["ASC", "DESC"]
|
|
54
56
|
|
|
55
57
|
|
|
58
|
+
def allow_branching(fn):
|
|
59
|
+
"""
|
|
60
|
+
Allows query method modifiers to implement their logic as if `self` is being
|
|
61
|
+
modified, but in the background we'll actually return a new instance of the
|
|
62
|
+
query builder to allow for branching of the same underlying query.
|
|
63
|
+
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
@wraps(fn)
|
|
67
|
+
def new_fn(self, *args, **kwargs):
|
|
68
|
+
self = copy(self)
|
|
69
|
+
return fn(self, *args, **kwargs)
|
|
70
|
+
|
|
71
|
+
return new_fn
|
|
72
|
+
|
|
73
|
+
|
|
56
74
|
class QueryBuilder(Generic[P, QueryType]):
|
|
57
75
|
"""
|
|
58
76
|
The QueryBuilder owns all construction of the SQL string given
|
|
@@ -118,6 +136,7 @@ class QueryBuilder(Generic[P, QueryType]):
|
|
|
118
136
|
self, fields: tuple[T | Type[T], T2 | Type[T2], T3 | Type[T3], *Ts]
|
|
119
137
|
) -> QueryBuilder[tuple[T, T2, T3, *Ts], Literal["SELECT"]]: ...
|
|
120
138
|
|
|
139
|
+
@allow_branching
|
|
121
140
|
def select(
|
|
122
141
|
self,
|
|
123
142
|
fields: (
|
|
@@ -212,6 +231,7 @@ class QueryBuilder(Generic[P, QueryType]):
|
|
|
212
231
|
self.select_raw.append(field)
|
|
213
232
|
self.select_aggregate_count += 1
|
|
214
233
|
|
|
234
|
+
@allow_branching
|
|
215
235
|
def update(self, model: Type[TableBase]) -> QueryBuilder[None, Literal["UPDATE"]]:
|
|
216
236
|
"""
|
|
217
237
|
Creates a new update query for the given model. Returns the same
|
|
@@ -222,6 +242,7 @@ class QueryBuilder(Generic[P, QueryType]):
|
|
|
222
242
|
self.main_model = model
|
|
223
243
|
return self # type: ignore
|
|
224
244
|
|
|
245
|
+
@allow_branching
|
|
225
246
|
def delete(self, model: Type[TableBase]) -> QueryBuilder[None, Literal["DELETE"]]:
|
|
226
247
|
"""
|
|
227
248
|
Creates a new delete query for the given model. Returns the same
|
|
@@ -232,6 +253,7 @@ class QueryBuilder(Generic[P, QueryType]):
|
|
|
232
253
|
self.main_model = model
|
|
233
254
|
return self # type: ignore
|
|
234
255
|
|
|
256
|
+
@allow_branching
|
|
235
257
|
def where(self, *conditions: bool):
|
|
236
258
|
"""
|
|
237
259
|
Adds a where condition to the query. The conditions are combined with
|
|
@@ -250,6 +272,7 @@ class QueryBuilder(Generic[P, QueryType]):
|
|
|
250
272
|
self.where_conditions += validated_comparisons
|
|
251
273
|
return self
|
|
252
274
|
|
|
275
|
+
@allow_branching
|
|
253
276
|
def order_by(self, field: Any, direction: OrderDirection = "ASC"):
|
|
254
277
|
"""
|
|
255
278
|
Adds an order by clause to the query. The field must be a column.
|
|
@@ -265,6 +288,7 @@ class QueryBuilder(Generic[P, QueryType]):
|
|
|
265
288
|
self.order_by_clauses.append(f"{field_token} {direction}")
|
|
266
289
|
return self
|
|
267
290
|
|
|
291
|
+
@allow_branching
|
|
268
292
|
def join(self, table: Type[TableBase], on: bool, join_type: JoinType = "INNER"):
|
|
269
293
|
"""
|
|
270
294
|
Adds a join clause to the query. The `on` parameter should be a comparison
|
|
@@ -289,6 +313,7 @@ class QueryBuilder(Generic[P, QueryType]):
|
|
|
289
313
|
self.join_clauses.append(join_sql)
|
|
290
314
|
return self
|
|
291
315
|
|
|
316
|
+
@allow_branching
|
|
292
317
|
def set(self, column: T, value: T | None):
|
|
293
318
|
"""
|
|
294
319
|
Sets a column to a specific value in an update query.
|
|
@@ -300,6 +325,7 @@ class QueryBuilder(Generic[P, QueryType]):
|
|
|
300
325
|
self.update_values.append((column, value))
|
|
301
326
|
return self
|
|
302
327
|
|
|
328
|
+
@allow_branching
|
|
303
329
|
def limit(self, value: int):
|
|
304
330
|
"""
|
|
305
331
|
Limit the number of rows returned by the query. Useful in pagination
|
|
@@ -309,6 +335,7 @@ class QueryBuilder(Generic[P, QueryType]):
|
|
|
309
335
|
self.limit_value = value
|
|
310
336
|
return self
|
|
311
337
|
|
|
338
|
+
@allow_branching
|
|
312
339
|
def offset(self, value: int):
|
|
313
340
|
"""
|
|
314
341
|
Offset the number of rows returned by the query.
|
|
@@ -317,6 +344,7 @@ class QueryBuilder(Generic[P, QueryType]):
|
|
|
317
344
|
self.offset_value = value
|
|
318
345
|
return self
|
|
319
346
|
|
|
347
|
+
@allow_branching
|
|
320
348
|
def group_by(self, *fields: Any):
|
|
321
349
|
"""
|
|
322
350
|
Groups the results of the query by the given fields. This allows
|
|
@@ -334,6 +362,7 @@ class QueryBuilder(Generic[P, QueryType]):
|
|
|
334
362
|
self.group_by_fields = valid_fields
|
|
335
363
|
return self
|
|
336
364
|
|
|
365
|
+
@allow_branching
|
|
337
366
|
def having(self, *conditions: bool):
|
|
338
367
|
"""
|
|
339
368
|
Require the result of an aggregation query like func.sum(MyTable.column)
|
|
@@ -351,6 +380,7 @@ class QueryBuilder(Generic[P, QueryType]):
|
|
|
351
380
|
self.having_conditions += valid_conditions
|
|
352
381
|
return self
|
|
353
382
|
|
|
383
|
+
@allow_branching
|
|
354
384
|
def text(self, query: str, *variables: Any):
|
|
355
385
|
"""
|
|
356
386
|
Override the ORM builder and use a raw SQL query instead.
|
|
@@ -98,6 +98,7 @@ cdef list process_values(
|
|
|
98
98
|
cdef object field_value
|
|
99
99
|
cdef object select_raw
|
|
100
100
|
cdef PyObject* temp_obj
|
|
101
|
+
cdef bint all_none
|
|
101
102
|
|
|
102
103
|
for i in range(num_values):
|
|
103
104
|
value = values[i]
|
|
@@ -112,6 +113,9 @@ cdef list process_values(
|
|
|
112
113
|
if raw_is_table:
|
|
113
114
|
obj_dict = {}
|
|
114
115
|
num_fields = num_fields_array[j]
|
|
116
|
+
all_none = True
|
|
117
|
+
|
|
118
|
+
# First pass: collect all fields and check if they're all None
|
|
115
119
|
for k in range(num_fields):
|
|
116
120
|
field_name_c = fields[j][k].name
|
|
117
121
|
select_name_c = fields[j][k].select_attribute
|
|
@@ -119,22 +123,29 @@ cdef list process_values(
|
|
|
119
123
|
select_name = select_name_c.decode('utf-8')
|
|
120
124
|
|
|
121
125
|
try:
|
|
122
|
-
field_value = value[select_name]
|
|
126
|
+
field_value = value[select_name]
|
|
123
127
|
except KeyError:
|
|
124
128
|
raise KeyError(f"Key '{select_name}' not found in value.")
|
|
125
129
|
|
|
126
|
-
if
|
|
127
|
-
|
|
130
|
+
if field_value is not None:
|
|
131
|
+
all_none = False
|
|
132
|
+
if fields[j][k].is_json:
|
|
133
|
+
field_value = json_loads(field_value)
|
|
128
134
|
|
|
129
135
|
obj_dict[field_name] = field_value
|
|
130
136
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
137
|
+
# If all fields are None, store None instead of creating the table object
|
|
138
|
+
if all_none:
|
|
139
|
+
result_value[j] = <PyObject*>None
|
|
140
|
+
Py_INCREF(None)
|
|
141
|
+
else:
|
|
142
|
+
obj = select_raw(**obj_dict)
|
|
143
|
+
result_value[j] = <PyObject*>obj
|
|
144
|
+
Py_INCREF(obj)
|
|
134
145
|
|
|
135
146
|
elif raw_is_column:
|
|
136
147
|
try:
|
|
137
|
-
item = value[select_raw.key]
|
|
148
|
+
item = value[select_raw.key]
|
|
138
149
|
except KeyError:
|
|
139
150
|
raise KeyError(f"Key '{select_raw.key}' not found in value.")
|
|
140
151
|
result_value[j] = <PyObject*>item
|
|
@@ -142,7 +153,7 @@ cdef list process_values(
|
|
|
142
153
|
|
|
143
154
|
elif raw_is_function_metadata:
|
|
144
155
|
try:
|
|
145
|
-
item = value[select_raw.local_name]
|
|
156
|
+
item = value[select_raw.local_name]
|
|
146
157
|
except KeyError:
|
|
147
158
|
raise KeyError(f"Key '{select_raw.local_name}' not found in value.")
|
|
148
159
|
result_value[j] = <PyObject*>item
|
|
@@ -151,11 +162,11 @@ cdef list process_values(
|
|
|
151
162
|
# Assemble the result
|
|
152
163
|
if num_selects == 1:
|
|
153
164
|
result_all[i] = <object>result_value[0]
|
|
154
|
-
Py_DECREF(<object>result_value[0])
|
|
165
|
+
Py_DECREF(<object>result_value[0])
|
|
155
166
|
else:
|
|
156
167
|
result_tuple = tuple([<object>result_value[j] for j in range(num_selects)])
|
|
157
168
|
for j in range(num_selects):
|
|
158
|
-
Py_DECREF(<object>result_value[j])
|
|
169
|
+
Py_DECREF(<object>result_value[j])
|
|
159
170
|
result_all[i] = result_tuple
|
|
160
171
|
|
|
161
172
|
finally:
|
|
@@ -22,7 +22,7 @@ install_requires = \
|
|
|
22
22
|
|
|
23
23
|
setup_kwargs = {
|
|
24
24
|
'name': 'iceaxe',
|
|
25
|
-
'version': '0.2.3.
|
|
25
|
+
'version': '0.2.3.dev3',
|
|
26
26
|
'description': 'A modern, fast ORM for Python.',
|
|
27
27
|
'long_description': '# iceaxe\n\nA modern, fast ORM for Python. We have the following goals:\n\n- 🏎️ **Performance**: We want to exceed or match the fastest ORMs in Python. We want our ORM\nto be as close as possible to raw-[asyncpg](https://github.com/MagicStack/asyncpg) speeds. See the "Benchmarks" section for more.\n- 📝 **Typehinting**: Everything should be typehinted with expected types. Declare your data as you\nexpect in Python and it should bidirectionally sync to the database.\n- 🐘 **Postgres only**: Leverage native Postgres features and simplify the implementation.\n- ⚡ **Common things are easy, rare things are possible**: 99% of the SQL queries we write are\nvanilla SELECT/INSERT/UPDATEs. These should be natively supported by your ORM. If you\'re writing _really_\ncomplex queries, these are better done by hand so you can see exactly what SQL will be run.\n\nIceaxe is in early alpha. It\'s also an independent project. It\'s compatible with the [Mountaineer](https://github.com/piercefreeman/mountaineer) ecosystem, but you can use it in whatever\nproject and web framework you\'re using.\n\n## Installation\n\nIf you\'re using poetry to manage your dependencies:\n\n```bash\npoetry add iceaxe\n```\n\nOtherwise install with pip:\n\n```bash\npip install iceaxe\n```\n\n## Usage\n\nDefine your models as a `TableBase` subclass:\n\n```python\nfrom iceaxe import TableBase\n\nclass Person(TableBase):\n id: int\n name: str\n age: int\n```\n\nTableBase is a subclass of Pydantic\'s `BaseModel`, so you get all of the validation and Field customization\nout of the box. We provide our own `Field` constructor that adds database-specific configuration. For instance, to make the\n`id` field a primary key / auto-incrementing you can do:\n\n```python\nfrom iceaxe import Field\n\nclass Person(TableBase):\n id: int = Field(primary_key=True)\n name: str\n age: int\n```\n\nOkay now you have a model. How do you interact with it?\n\nDatabases are based on a few core primitives to insert data, update it, and fetch it out again.\nTo do so you\'ll need a _database connection_, which is a connection over the network from your code\nto your Postgres database. The `DBConnection` is the core class for all ORM actions against the database.\n\n```python\nfrom iceaxe import DBConnection\nimport asyncpg\n\nconn = DBConnection(\n await asyncpg.connect(\n host="localhost",\n port=5432,\n user="db_user",\n password="yoursecretpassword",\n database="your_db",\n )\n)\n```\n\nThe Person class currently just lives in memory. To back it with a full\ndatabase table, we can run raw SQL or run a migration to add it:\n\n```python\nawait conn.conn.execute(\n """\n CREATE TABLE IF NOT EXISTS person (\n id SERIAL PRIMARY KEY,\n name TEXT NOT NULL,\n age INT NOT NULL\n )\n """\n)\n```\n\n### Inserting Data\n\nInstantiate object classes as you normally do:\n\n```python\npeople = [\n Person(name="Alice", age=30),\n Person(name="Bob", age=40),\n Person(name="Charlie", age=50),\n]\nawait conn.insert(people)\n\nprint(people[0].id) # 1\nprint(people[1].id) # 2\n```\n\nBecause we\'re using an auto-incrementing primary key, the `id` field will be populated after the insert.\nIceaxe will automatically update the object in place with the newly assigned value.\n\n### Updating data\n\nNow that we have these lovely people, let\'s modify them.\n\n```python\nperson = people[0]\nperson.name = "Blice"\n```\n\nRight now, we have a Python object that\'s out of state with the database. But that\'s often okay. We can inspect it\nand further write logic - it\'s fully decoupled from the database.\n\n```python\ndef ensure_b_letter(person: Person):\n if person.name[0].lower() != "b":\n raise ValueError("Name must start with \'B\'")\n\nensure_b_letter(person)\n```\n\nTo sync the values back to the database, we can call `update`:\n\n```python\nawait conn.update([person])\n```\n\nIf we were to query the database directly, we see that the name has been updated:\n\n```\nid | name | age\n----+-------+-----\n 1 | Blice | 31\n 2 | Bob | 40\n 3 | Charlie | 50\n```\n\nBut no other fields have been touched. This lets a potentially concurrent process\nmodify `Alice`\'s record - say, updating the age to 31. By the time we update the data, we\'ll\nchange the name but nothing else. Under the hood we do this by tracking the fields that\nhave been modified in-memory and creating a targeted UPDATE to modify only those values.\n\n### Selecting data\n\nTo select data, we can use a `QueryBuilder`. For a shortcut to `select` query functions,\nyou can also just import select directly. This method takes the desired value parameters\nand returns a list of the desired objects.\n\n```python\nfrom iceaxe import select\n\nquery = select(Person).where(Person.name == "Blice", Person.age > 25)\nresults = await conn.exec(query)\n```\n\nIf we inspect the typing of `results`, we see that it\'s a `list[Person]` objects. This matches\nthe typehint of the `select` function. You can also target columns directly:\n\n```python\nquery = select((Person.id, Person.name)).where(Person.age > 25)\nresults = await conn.exec(query)\n```\n\nThis will return a list of tuples, where each tuple is the id and name of the person: `list[tuple[int, str]]`.\n\nWe support most of the common SQL operations. Just like the results, these are typehinted\nto their proper types as well. Static typecheckers and your IDE will throw an error if you try to compare\na string column to an integer, for instance. A more complex example of a query:\n\n```python\nquery = select((\n Person.id,\n FavoriteColor,\n)).join(\n FavoriteColor,\n Person.id == FavoriteColor.person_id,\n).where(\n Person.age > 25,\n Person.name == "Blice",\n).order_by(\n Person.age.desc(),\n).limit(10)\nresults = await conn.exec(query)\n```\n\nAs expected this will deliver results - and typehint - as a `list[tuple[int, FavoriteColor]]`\n\n## Production\n\n> [!IMPORTANT]\n> Iceaxe is in early alpha. We\'re using it internally and showly rolling out to our production\napplications, but we\'re not yet ready to recommend it for general use. The API and larger\nstability is subject to change.\n\nNote that underlying Postgres connection wrapped by `conn` will be alive for as long as your object is in memory. This uses up one\nof the allowable connections to your database. Your overall limit depends on your Postgres configuration\nor hosting provider, but most managed solutions top out around 150-300. If you need more concurrent clients\nconnected (and even if you don\'t - connection creation at the Postgres level is expensive), you can adopt\na load balancer like `pgbouncer` to better scale to traffic. More deployment notes to come.\n\nIt\'s also worth noting the absence of request pooling in this initialization. This is a feature of many ORMs that lets you limit\nthe overall connections you make to Postgres, and re-use these over time. We specifically don\'t offer request\npooling as part of Iceaxe, despite being supported by our underlying engine `asyncpg`. This is a bit more\naligned to how things should be structured in production. Python apps are always bound to one process thanks to\nthe GIL. So no matter what your connection pool will always be tied to the current Python process / runtime. When you\'re deploying onto a server with multiple cores, the pool will be duplicated across CPUs and largely defeats the purpose of capping\nnetwork connections in the first place.\n\n## Benchmarking\n\nWe have basic benchmarking tests in the `__tests__/benchmarks` directory. To run them, you\'ll need to execute the pytest suite:\n\n```bash\npoetry run pytest -m integration_tests\n```\n\nCurrent benchmarking as of October 11 2024 is:\n\n| | raw asyncpg | iceaxe | external overhead | |\n|-------------------|-------------|--------|-----------------------------------------------|---|\n| TableBase columns | 0.098s | 0.093s | | |\n| TableBase full | 0.164s | 1.345s | 10%: dict construction | 90%: pydantic overhead | |\n\n## Development\n\nIf you update your Cython implementation during development, you\'ll need to re-compile the Cython code. This can be done with\na simple poetry install. Poetry is set up to create a dynamic `setup.py` based on our `build.py` definition.\n\n```bash\npoetry install\n```\n\n## TODOs\n\n- [ ] Additional documentation with usage examples.\n',
|
|
28
28
|
'author': 'Pierce Freeman',
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{iceaxe-0.2.3.dev2 → iceaxe-0.2.3.dev3}/iceaxe/__tests__/mountaineer/dependencies/__init__.py
RENAMED
|
File without changes
|
{iceaxe-0.2.3.dev2 → iceaxe-0.2.3.dev3}/iceaxe/__tests__/mountaineer/dependencies/test_core.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{iceaxe-0.2.3.dev2 → iceaxe-0.2.3.dev3}/iceaxe/__tests__/schemas/test_db_memory_serializer.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|