gault 0.1.0__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.
- gault-0.1.0/PKG-INFO +298 -0
- gault-0.1.0/README.md +277 -0
- gault-0.1.0/pyproject.toml +68 -0
- gault-0.1.0/src/gault/__init__.py +22 -0
- gault-0.1.0/src/gault/accumulators.py +325 -0
- gault-0.1.0/src/gault/compilers.py +83 -0
- gault-0.1.0/src/gault/exceptions.py +48 -0
- gault-0.1.0/src/gault/expressions.py +3118 -0
- gault-0.1.0/src/gault/geo.py +30 -0
- gault-0.1.0/src/gault/managers.py +394 -0
- gault-0.1.0/src/gault/mappers.py +87 -0
- gault-0.1.0/src/gault/models.py +132 -0
- gault-0.1.0/src/gault/pipelines.py +642 -0
- gault-0.1.0/src/gault/predicates.py +783 -0
- gault-0.1.0/src/gault/py.typed +0 -0
- gault-0.1.0/src/gault/sorting.py +59 -0
- gault-0.1.0/src/gault/types.py +168 -0
- gault-0.1.0/src/gault/utils.py +37 -0
- gault-0.1.0/src/gault/window_aggregators.py +211 -0
gault-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: gault
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A lightweight Object Document Mapper for MongoDB
|
|
5
|
+
Keywords: mongodb,odm,document-mapper,orm,database
|
|
6
|
+
Author: Xavier Barbosa
|
|
7
|
+
Author-email: Xavier Barbosa <clint.northwood@gmail.com>
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Topic :: Database
|
|
13
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
14
|
+
Requires-Dist: annotated-types>=0.7.0
|
|
15
|
+
Requires-Dist: pymongo>=4.15.4
|
|
16
|
+
Requires-Python: >=3.12
|
|
17
|
+
Project-URL: Homepage, https://github.com/johnoone/gault
|
|
18
|
+
Project-URL: Issues, https://github.com/johnoone/gault/issues
|
|
19
|
+
Project-URL: Repository, https://github.com/johnoone/gault.git
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
|
|
22
|
+
# Strata
|
|
23
|
+
|
|
24
|
+
A lightweight Object Document Mapper (ODM) for MongoDB with Python type hints and state tracking.
|
|
25
|
+
|
|
26
|
+
## Features
|
|
27
|
+
|
|
28
|
+
- Type-safe MongoDB documents with Python type hints
|
|
29
|
+
- Field aliasing for database column mapping
|
|
30
|
+
- Query operators with Pythonic syntax
|
|
31
|
+
- Async manager for CRUD operations
|
|
32
|
+
- Aggregation pipeline support
|
|
33
|
+
- Automatic state tracking and dirty field detection
|
|
34
|
+
- Persistence tracking
|
|
35
|
+
|
|
36
|
+
## Installation
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
pip install gault
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Quick Start
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
from gault import Schema, Model, Field, configure, AsyncManager
|
|
46
|
+
|
|
47
|
+
# Schema: Persistent documents mapped to MongoDB collections
|
|
48
|
+
class Person(Schema, collection="people"):
|
|
49
|
+
id: Field[int] = configure(pk=True)
|
|
50
|
+
name: Field[str]
|
|
51
|
+
age: Field[int] = configure(db_alias="person_age")
|
|
52
|
+
|
|
53
|
+
# Model: Non-persistent data classes (projections, view models, etc.)
|
|
54
|
+
class PersonSummary(Model):
|
|
55
|
+
name: Field[str]
|
|
56
|
+
total: Field[int]
|
|
57
|
+
|
|
58
|
+
# Create manager
|
|
59
|
+
manager = AsyncManager(database)
|
|
60
|
+
|
|
61
|
+
# Query and modify
|
|
62
|
+
person = await manager.get(Person, filter=Person.id == 1)
|
|
63
|
+
person.age = 43
|
|
64
|
+
await manager.save(person, atomic=True) # Only updates dirty fields
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Schema vs Model
|
|
68
|
+
|
|
69
|
+
- **Schema**: Persistent MongoDB collections. Requires `collection` parameter and gets registered globally.
|
|
70
|
+
- **Model**: Non-persistent data structures for aggregation projections, view models, or intermediate transformations.
|
|
71
|
+
|
|
72
|
+
## Field Configuration
|
|
73
|
+
|
|
74
|
+
Fields can be configured with metadata using the `configure()` function:
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
class Person(Schema, collection="people"):
|
|
78
|
+
# Primary key field - used for filtering in save() operations
|
|
79
|
+
id: Field[int] = configure(pk=True)
|
|
80
|
+
|
|
81
|
+
# Regular field
|
|
82
|
+
name: Field[str]
|
|
83
|
+
|
|
84
|
+
# Field with database alias (field name differs from DB column)
|
|
85
|
+
age: Field[int] = configure(db_alias="person_age")
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
**Note**: Fields marked with `pk=True` are used as the filter criteria in `save()` operations to identify the document for upsert.
|
|
89
|
+
|
|
90
|
+
## Querying with Filters
|
|
91
|
+
|
|
92
|
+
Strata provides multiple ways to filter documents using type-safe field expressions.
|
|
93
|
+
|
|
94
|
+
### Operator Expressions
|
|
95
|
+
|
|
96
|
+
Use class fields with comparison operators to build type-safe queries:
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
# Comparison operators
|
|
100
|
+
Person.age == 42 # Equal
|
|
101
|
+
Person.age != 30 # Not equal
|
|
102
|
+
Person.age < 50 # Less than
|
|
103
|
+
Person.age <= 50 # Less than or equal
|
|
104
|
+
Person.age > 18 # Greater than
|
|
105
|
+
Person.age >= 18 # Greater than or equal
|
|
106
|
+
Person.id.in_([1, 2, 3]) # In list
|
|
107
|
+
Person.id.nin([4, 5]) # Not in list
|
|
108
|
+
|
|
109
|
+
# Logical operators
|
|
110
|
+
filter = (Person.age >= 18) & (Person.age < 65) # AND
|
|
111
|
+
filter = (Person.name == "Alice") | (Person.name == "Bob") # OR
|
|
112
|
+
filter = ~(Person.age < 18) # NOT
|
|
113
|
+
|
|
114
|
+
# Complex expressions
|
|
115
|
+
filter = (Person.age >= 18) & ((Person.name == "Alice") | (Person.name == "Bob"))
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### Pipeline Filters
|
|
119
|
+
|
|
120
|
+
For advanced queries, use the `Pipeline` API with aggregation stages:
|
|
121
|
+
|
|
122
|
+
```python
|
|
123
|
+
from gault import Pipeline
|
|
124
|
+
|
|
125
|
+
# Match and sort
|
|
126
|
+
pipeline = Pipeline().match(Person.age >= 18).sort(Person.age.asc())
|
|
127
|
+
|
|
128
|
+
# Pagination
|
|
129
|
+
pipeline = Pipeline().skip(10).take(20)
|
|
130
|
+
|
|
131
|
+
# Group and aggregate
|
|
132
|
+
from gault import Sum
|
|
133
|
+
pipeline = (
|
|
134
|
+
Pipeline()
|
|
135
|
+
.match(Person.age >= 18)
|
|
136
|
+
.group(by=Person.name, accumulators={"total": Sum(Person.age)})
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
# Multiple stages
|
|
140
|
+
pipeline = (
|
|
141
|
+
Pipeline()
|
|
142
|
+
.match(Person.age >= 18)
|
|
143
|
+
.sort(Person.age.desc())
|
|
144
|
+
.take(10)
|
|
145
|
+
)
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### Raw MongoDB Queries
|
|
149
|
+
|
|
150
|
+
You can also use raw MongoDB query dictionaries:
|
|
151
|
+
|
|
152
|
+
```python
|
|
153
|
+
# Dict filter
|
|
154
|
+
filter = {"age": {"$gte": 18}}
|
|
155
|
+
|
|
156
|
+
# Raw pipeline stages
|
|
157
|
+
pipeline = [
|
|
158
|
+
{"$match": {"age": {"$gte": 18}}},
|
|
159
|
+
{"$sort": {"age": -1}},
|
|
160
|
+
{"$limit": 10}
|
|
161
|
+
]
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## AsyncManager Methods
|
|
165
|
+
|
|
166
|
+
### `find(model, filter=None)`
|
|
167
|
+
Finds a single document matching the filter. Returns `None` if not found.
|
|
168
|
+
|
|
169
|
+
**Filter types**: Operator expression, Pipeline, dict, or list of stages.
|
|
170
|
+
|
|
171
|
+
```python
|
|
172
|
+
# With operator
|
|
173
|
+
person = await manager.find(Person, filter=Person.age == 42)
|
|
174
|
+
|
|
175
|
+
# With pipeline
|
|
176
|
+
pipeline = Pipeline().match(Person.age > 30).sort(Person.name.asc())
|
|
177
|
+
person = await manager.find(Person, filter=pipeline)
|
|
178
|
+
|
|
179
|
+
# With dict
|
|
180
|
+
person = await manager.find(Person, filter={"age": 42})
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### `get(model, filter=None)`
|
|
184
|
+
Like `find()`, but raises `NotFound` exception if no document is found.
|
|
185
|
+
|
|
186
|
+
**Filter types**: Operator expression, Pipeline, dict, or list of stages.
|
|
187
|
+
|
|
188
|
+
```python
|
|
189
|
+
try:
|
|
190
|
+
person = await manager.get(Person, filter=Person.id == 123)
|
|
191
|
+
except NotFound:
|
|
192
|
+
print("Person not found")
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### `select(model, filter=None, skip=None, take=None)`
|
|
196
|
+
Returns an async iterator of documents matching the filter. Supports pagination.
|
|
197
|
+
|
|
198
|
+
**Filter types**: Operator expression, Pipeline, dict, or list of stages.
|
|
199
|
+
|
|
200
|
+
```python
|
|
201
|
+
# Operator with in_()
|
|
202
|
+
async for person in manager.select(Person, filter=Person.id.in_([1, 2, 3])):
|
|
203
|
+
print(person.name)
|
|
204
|
+
|
|
205
|
+
# Pipeline
|
|
206
|
+
pipeline = Pipeline().match(Person.age >= 18).sort(Person.age.desc())
|
|
207
|
+
async for person in manager.select(Person, filter=pipeline, take=10):
|
|
208
|
+
print(person.name)
|
|
209
|
+
|
|
210
|
+
# Complex filter
|
|
211
|
+
filter = (Person.age >= 18) & (Person.age < 65)
|
|
212
|
+
async for person in manager.select(Person, filter=filter):
|
|
213
|
+
print(person.name)
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
### `insert(instance)`
|
|
217
|
+
Inserts a new document into the database. Only works with `Schema` instances.
|
|
218
|
+
|
|
219
|
+
```python
|
|
220
|
+
new_person = Person(id=1, name="Alice", age=30)
|
|
221
|
+
await manager.insert(new_person)
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
### `save(instance, refresh=False, atomic=False)`
|
|
225
|
+
Upserts a document using `find_one_and_update`. Supports atomic updates with dirty field tracking.
|
|
226
|
+
|
|
227
|
+
- **`refresh=False`**: If `True`, refreshes the instance with the document returned from the database
|
|
228
|
+
- **`atomic=False`**: If `True` and the instance is already persisted, only updates dirty fields
|
|
229
|
+
|
|
230
|
+
```python
|
|
231
|
+
# Create or update
|
|
232
|
+
person = Person(id=1, name="Bob", age=25)
|
|
233
|
+
await manager.save(person)
|
|
234
|
+
|
|
235
|
+
# Later, update only changed fields
|
|
236
|
+
person.age = 26
|
|
237
|
+
await manager.save(person, atomic=True) # Only updates 'person_age' field
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
## Persistence and Dirty Fields
|
|
241
|
+
|
|
242
|
+
Strata tracks the persistence state and modifications of your documents automatically.
|
|
243
|
+
|
|
244
|
+
### Persistence Tracking
|
|
245
|
+
|
|
246
|
+
When documents are loaded from the database or saved, they are marked as persisted:
|
|
247
|
+
|
|
248
|
+
```python
|
|
249
|
+
# Loaded from DB - automatically marked as persisted
|
|
250
|
+
person = await manager.find(Person, filter=Person.id == 1)
|
|
251
|
+
assert manager.persistence.is_persisted(person)
|
|
252
|
+
|
|
253
|
+
# Newly created - not yet persisted
|
|
254
|
+
new_person = Person(id=2, name="Charlie", age=35)
|
|
255
|
+
assert not manager.persistence.is_persisted(new_person)
|
|
256
|
+
|
|
257
|
+
# After saving - marked as persisted
|
|
258
|
+
await manager.save(new_person)
|
|
259
|
+
assert manager.persistence.is_persisted(new_person)
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
### Dirty Field Tracking
|
|
263
|
+
|
|
264
|
+
Strata snapshots document state and tracks which fields have been modified:
|
|
265
|
+
|
|
266
|
+
```python
|
|
267
|
+
person = await manager.get(Person, filter=Person.id == 1)
|
|
268
|
+
|
|
269
|
+
# Modify some fields
|
|
270
|
+
person.name = "New Name"
|
|
271
|
+
person.age = 50
|
|
272
|
+
|
|
273
|
+
# Check which fields changed
|
|
274
|
+
dirty_fields = manager.state_tracker.get_dirty_fields(person)
|
|
275
|
+
# dirty_fields == {'name', 'age'}
|
|
276
|
+
|
|
277
|
+
# Atomic save only updates changed fields
|
|
278
|
+
await manager.save(person, atomic=True)
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
### Atomic Updates
|
|
282
|
+
|
|
283
|
+
When using `atomic=True`, the `save()` method generates optimal MongoDB updates:
|
|
284
|
+
|
|
285
|
+
- **Dirty fields**: Updated with `$set`
|
|
286
|
+
- **Unchanged fields**: Set with `$setOnInsert` (only on insert, not update)
|
|
287
|
+
- **Primary key fields**: Used in the filter
|
|
288
|
+
|
|
289
|
+
This minimizes race conditions and reduces unnecessary updates.
|
|
290
|
+
|
|
291
|
+
## Requirements
|
|
292
|
+
|
|
293
|
+
- Python >= 3.12
|
|
294
|
+
- PyMongo >= 4.15.4
|
|
295
|
+
|
|
296
|
+
## License
|
|
297
|
+
|
|
298
|
+
MIT
|
gault-0.1.0/README.md
ADDED
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
# Strata
|
|
2
|
+
|
|
3
|
+
A lightweight Object Document Mapper (ODM) for MongoDB with Python type hints and state tracking.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- Type-safe MongoDB documents with Python type hints
|
|
8
|
+
- Field aliasing for database column mapping
|
|
9
|
+
- Query operators with Pythonic syntax
|
|
10
|
+
- Async manager for CRUD operations
|
|
11
|
+
- Aggregation pipeline support
|
|
12
|
+
- Automatic state tracking and dirty field detection
|
|
13
|
+
- Persistence tracking
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pip install gault
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Quick Start
|
|
22
|
+
|
|
23
|
+
```python
|
|
24
|
+
from gault import Schema, Model, Field, configure, AsyncManager
|
|
25
|
+
|
|
26
|
+
# Schema: Persistent documents mapped to MongoDB collections
|
|
27
|
+
class Person(Schema, collection="people"):
|
|
28
|
+
id: Field[int] = configure(pk=True)
|
|
29
|
+
name: Field[str]
|
|
30
|
+
age: Field[int] = configure(db_alias="person_age")
|
|
31
|
+
|
|
32
|
+
# Model: Non-persistent data classes (projections, view models, etc.)
|
|
33
|
+
class PersonSummary(Model):
|
|
34
|
+
name: Field[str]
|
|
35
|
+
total: Field[int]
|
|
36
|
+
|
|
37
|
+
# Create manager
|
|
38
|
+
manager = AsyncManager(database)
|
|
39
|
+
|
|
40
|
+
# Query and modify
|
|
41
|
+
person = await manager.get(Person, filter=Person.id == 1)
|
|
42
|
+
person.age = 43
|
|
43
|
+
await manager.save(person, atomic=True) # Only updates dirty fields
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Schema vs Model
|
|
47
|
+
|
|
48
|
+
- **Schema**: Persistent MongoDB collections. Requires `collection` parameter and gets registered globally.
|
|
49
|
+
- **Model**: Non-persistent data structures for aggregation projections, view models, or intermediate transformations.
|
|
50
|
+
|
|
51
|
+
## Field Configuration
|
|
52
|
+
|
|
53
|
+
Fields can be configured with metadata using the `configure()` function:
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
class Person(Schema, collection="people"):
|
|
57
|
+
# Primary key field - used for filtering in save() operations
|
|
58
|
+
id: Field[int] = configure(pk=True)
|
|
59
|
+
|
|
60
|
+
# Regular field
|
|
61
|
+
name: Field[str]
|
|
62
|
+
|
|
63
|
+
# Field with database alias (field name differs from DB column)
|
|
64
|
+
age: Field[int] = configure(db_alias="person_age")
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
**Note**: Fields marked with `pk=True` are used as the filter criteria in `save()` operations to identify the document for upsert.
|
|
68
|
+
|
|
69
|
+
## Querying with Filters
|
|
70
|
+
|
|
71
|
+
Strata provides multiple ways to filter documents using type-safe field expressions.
|
|
72
|
+
|
|
73
|
+
### Operator Expressions
|
|
74
|
+
|
|
75
|
+
Use class fields with comparison operators to build type-safe queries:
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
# Comparison operators
|
|
79
|
+
Person.age == 42 # Equal
|
|
80
|
+
Person.age != 30 # Not equal
|
|
81
|
+
Person.age < 50 # Less than
|
|
82
|
+
Person.age <= 50 # Less than or equal
|
|
83
|
+
Person.age > 18 # Greater than
|
|
84
|
+
Person.age >= 18 # Greater than or equal
|
|
85
|
+
Person.id.in_([1, 2, 3]) # In list
|
|
86
|
+
Person.id.nin([4, 5]) # Not in list
|
|
87
|
+
|
|
88
|
+
# Logical operators
|
|
89
|
+
filter = (Person.age >= 18) & (Person.age < 65) # AND
|
|
90
|
+
filter = (Person.name == "Alice") | (Person.name == "Bob") # OR
|
|
91
|
+
filter = ~(Person.age < 18) # NOT
|
|
92
|
+
|
|
93
|
+
# Complex expressions
|
|
94
|
+
filter = (Person.age >= 18) & ((Person.name == "Alice") | (Person.name == "Bob"))
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Pipeline Filters
|
|
98
|
+
|
|
99
|
+
For advanced queries, use the `Pipeline` API with aggregation stages:
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
from gault import Pipeline
|
|
103
|
+
|
|
104
|
+
# Match and sort
|
|
105
|
+
pipeline = Pipeline().match(Person.age >= 18).sort(Person.age.asc())
|
|
106
|
+
|
|
107
|
+
# Pagination
|
|
108
|
+
pipeline = Pipeline().skip(10).take(20)
|
|
109
|
+
|
|
110
|
+
# Group and aggregate
|
|
111
|
+
from gault import Sum
|
|
112
|
+
pipeline = (
|
|
113
|
+
Pipeline()
|
|
114
|
+
.match(Person.age >= 18)
|
|
115
|
+
.group(by=Person.name, accumulators={"total": Sum(Person.age)})
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
# Multiple stages
|
|
119
|
+
pipeline = (
|
|
120
|
+
Pipeline()
|
|
121
|
+
.match(Person.age >= 18)
|
|
122
|
+
.sort(Person.age.desc())
|
|
123
|
+
.take(10)
|
|
124
|
+
)
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### Raw MongoDB Queries
|
|
128
|
+
|
|
129
|
+
You can also use raw MongoDB query dictionaries:
|
|
130
|
+
|
|
131
|
+
```python
|
|
132
|
+
# Dict filter
|
|
133
|
+
filter = {"age": {"$gte": 18}}
|
|
134
|
+
|
|
135
|
+
# Raw pipeline stages
|
|
136
|
+
pipeline = [
|
|
137
|
+
{"$match": {"age": {"$gte": 18}}},
|
|
138
|
+
{"$sort": {"age": -1}},
|
|
139
|
+
{"$limit": 10}
|
|
140
|
+
]
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## AsyncManager Methods
|
|
144
|
+
|
|
145
|
+
### `find(model, filter=None)`
|
|
146
|
+
Finds a single document matching the filter. Returns `None` if not found.
|
|
147
|
+
|
|
148
|
+
**Filter types**: Operator expression, Pipeline, dict, or list of stages.
|
|
149
|
+
|
|
150
|
+
```python
|
|
151
|
+
# With operator
|
|
152
|
+
person = await manager.find(Person, filter=Person.age == 42)
|
|
153
|
+
|
|
154
|
+
# With pipeline
|
|
155
|
+
pipeline = Pipeline().match(Person.age > 30).sort(Person.name.asc())
|
|
156
|
+
person = await manager.find(Person, filter=pipeline)
|
|
157
|
+
|
|
158
|
+
# With dict
|
|
159
|
+
person = await manager.find(Person, filter={"age": 42})
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### `get(model, filter=None)`
|
|
163
|
+
Like `find()`, but raises `NotFound` exception if no document is found.
|
|
164
|
+
|
|
165
|
+
**Filter types**: Operator expression, Pipeline, dict, or list of stages.
|
|
166
|
+
|
|
167
|
+
```python
|
|
168
|
+
try:
|
|
169
|
+
person = await manager.get(Person, filter=Person.id == 123)
|
|
170
|
+
except NotFound:
|
|
171
|
+
print("Person not found")
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### `select(model, filter=None, skip=None, take=None)`
|
|
175
|
+
Returns an async iterator of documents matching the filter. Supports pagination.
|
|
176
|
+
|
|
177
|
+
**Filter types**: Operator expression, Pipeline, dict, or list of stages.
|
|
178
|
+
|
|
179
|
+
```python
|
|
180
|
+
# Operator with in_()
|
|
181
|
+
async for person in manager.select(Person, filter=Person.id.in_([1, 2, 3])):
|
|
182
|
+
print(person.name)
|
|
183
|
+
|
|
184
|
+
# Pipeline
|
|
185
|
+
pipeline = Pipeline().match(Person.age >= 18).sort(Person.age.desc())
|
|
186
|
+
async for person in manager.select(Person, filter=pipeline, take=10):
|
|
187
|
+
print(person.name)
|
|
188
|
+
|
|
189
|
+
# Complex filter
|
|
190
|
+
filter = (Person.age >= 18) & (Person.age < 65)
|
|
191
|
+
async for person in manager.select(Person, filter=filter):
|
|
192
|
+
print(person.name)
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### `insert(instance)`
|
|
196
|
+
Inserts a new document into the database. Only works with `Schema` instances.
|
|
197
|
+
|
|
198
|
+
```python
|
|
199
|
+
new_person = Person(id=1, name="Alice", age=30)
|
|
200
|
+
await manager.insert(new_person)
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
### `save(instance, refresh=False, atomic=False)`
|
|
204
|
+
Upserts a document using `find_one_and_update`. Supports atomic updates with dirty field tracking.
|
|
205
|
+
|
|
206
|
+
- **`refresh=False`**: If `True`, refreshes the instance with the document returned from the database
|
|
207
|
+
- **`atomic=False`**: If `True` and the instance is already persisted, only updates dirty fields
|
|
208
|
+
|
|
209
|
+
```python
|
|
210
|
+
# Create or update
|
|
211
|
+
person = Person(id=1, name="Bob", age=25)
|
|
212
|
+
await manager.save(person)
|
|
213
|
+
|
|
214
|
+
# Later, update only changed fields
|
|
215
|
+
person.age = 26
|
|
216
|
+
await manager.save(person, atomic=True) # Only updates 'person_age' field
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
## Persistence and Dirty Fields
|
|
220
|
+
|
|
221
|
+
Strata tracks the persistence state and modifications of your documents automatically.
|
|
222
|
+
|
|
223
|
+
### Persistence Tracking
|
|
224
|
+
|
|
225
|
+
When documents are loaded from the database or saved, they are marked as persisted:
|
|
226
|
+
|
|
227
|
+
```python
|
|
228
|
+
# Loaded from DB - automatically marked as persisted
|
|
229
|
+
person = await manager.find(Person, filter=Person.id == 1)
|
|
230
|
+
assert manager.persistence.is_persisted(person)
|
|
231
|
+
|
|
232
|
+
# Newly created - not yet persisted
|
|
233
|
+
new_person = Person(id=2, name="Charlie", age=35)
|
|
234
|
+
assert not manager.persistence.is_persisted(new_person)
|
|
235
|
+
|
|
236
|
+
# After saving - marked as persisted
|
|
237
|
+
await manager.save(new_person)
|
|
238
|
+
assert manager.persistence.is_persisted(new_person)
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
### Dirty Field Tracking
|
|
242
|
+
|
|
243
|
+
Strata snapshots document state and tracks which fields have been modified:
|
|
244
|
+
|
|
245
|
+
```python
|
|
246
|
+
person = await manager.get(Person, filter=Person.id == 1)
|
|
247
|
+
|
|
248
|
+
# Modify some fields
|
|
249
|
+
person.name = "New Name"
|
|
250
|
+
person.age = 50
|
|
251
|
+
|
|
252
|
+
# Check which fields changed
|
|
253
|
+
dirty_fields = manager.state_tracker.get_dirty_fields(person)
|
|
254
|
+
# dirty_fields == {'name', 'age'}
|
|
255
|
+
|
|
256
|
+
# Atomic save only updates changed fields
|
|
257
|
+
await manager.save(person, atomic=True)
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
### Atomic Updates
|
|
261
|
+
|
|
262
|
+
When using `atomic=True`, the `save()` method generates optimal MongoDB updates:
|
|
263
|
+
|
|
264
|
+
- **Dirty fields**: Updated with `$set`
|
|
265
|
+
- **Unchanged fields**: Set with `$setOnInsert` (only on insert, not update)
|
|
266
|
+
- **Primary key fields**: Used in the filter
|
|
267
|
+
|
|
268
|
+
This minimizes race conditions and reduces unnecessary updates.
|
|
269
|
+
|
|
270
|
+
## Requirements
|
|
271
|
+
|
|
272
|
+
- Python >= 3.12
|
|
273
|
+
- PyMongo >= 4.15.4
|
|
274
|
+
|
|
275
|
+
## License
|
|
276
|
+
|
|
277
|
+
MIT
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "gault"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "A lightweight Object Document Mapper for MongoDB"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
authors = [{ name = "Xavier Barbosa", email = "clint.northwood@gmail.com" }]
|
|
7
|
+
requires-python = ">=3.12"
|
|
8
|
+
dependencies = ["annotated-types>=0.7.0", "pymongo>=4.15.4"]
|
|
9
|
+
keywords = ["mongodb", "odm", "document-mapper", "orm", "database"]
|
|
10
|
+
classifiers = [
|
|
11
|
+
"Development Status :: 3 - Alpha",
|
|
12
|
+
"Intended Audience :: Developers",
|
|
13
|
+
"License :: OSI Approved :: MIT License",
|
|
14
|
+
"Programming Language :: Python :: 3",
|
|
15
|
+
"Topic :: Database",
|
|
16
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
17
|
+
]
|
|
18
|
+
[build-system]
|
|
19
|
+
requires = ["uv_build>=0.9.11,<0.10.0"]
|
|
20
|
+
build-backend = "uv_build"
|
|
21
|
+
|
|
22
|
+
[dependency-groups]
|
|
23
|
+
dev = [
|
|
24
|
+
"anyio>=4.11.0",
|
|
25
|
+
"mypy>=1.18.2",
|
|
26
|
+
"pytest>=9.0.1",
|
|
27
|
+
"pytest-cov>=7.0.0",
|
|
28
|
+
"pytest-subtests>=0.15.0",
|
|
29
|
+
"ruff>=0.14.6",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
[project.urls]
|
|
33
|
+
Homepage = "https://github.com/johnoone/gault"
|
|
34
|
+
Repository = "https://github.com/johnoone/gault.git"
|
|
35
|
+
Issues = "https://github.com/johnoone/gault/issues"
|
|
36
|
+
|
|
37
|
+
[tool.pytest.ini_options]
|
|
38
|
+
anyio_mode = "auto"
|
|
39
|
+
|
|
40
|
+
[tool.ruff]
|
|
41
|
+
target-version = "py312"
|
|
42
|
+
|
|
43
|
+
[tool.ruff.lint]
|
|
44
|
+
pylint.max-args = 8
|
|
45
|
+
extend-select = ["ALL"]
|
|
46
|
+
extend-ignore = [
|
|
47
|
+
"A001",
|
|
48
|
+
"A002",
|
|
49
|
+
"ARG001",
|
|
50
|
+
"ARG002",
|
|
51
|
+
"ANN401",
|
|
52
|
+
"COM812",
|
|
53
|
+
"D100",
|
|
54
|
+
"D101",
|
|
55
|
+
"D102",
|
|
56
|
+
"D103",
|
|
57
|
+
"D104",
|
|
58
|
+
"D105",
|
|
59
|
+
"D107",
|
|
60
|
+
"D203",
|
|
61
|
+
"D213",
|
|
62
|
+
"E501",
|
|
63
|
+
"F402",
|
|
64
|
+
"FBT003",
|
|
65
|
+
"N818",
|
|
66
|
+
"PLR2004",
|
|
67
|
+
"SIM108",
|
|
68
|
+
]
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from .accumulators import Accumulator as Accumulator
|
|
4
|
+
from .accumulators import Sum as Sum
|
|
5
|
+
from .exceptions import Forbidden as Forbidden
|
|
6
|
+
from .exceptions import NotFound as NotFound
|
|
7
|
+
from .exceptions import Unprocessable as Unprocessable
|
|
8
|
+
from .expressions import Var as Var
|
|
9
|
+
from .managers import AsyncManager as AsyncManager
|
|
10
|
+
from .managers import Manager as Manager
|
|
11
|
+
from .managers import Persistence as Persistence
|
|
12
|
+
from .managers import StateTracker as StateTracker
|
|
13
|
+
from .mappers import Mapper as Mapper
|
|
14
|
+
from .mappers import get_mapper as get_mapper
|
|
15
|
+
from .models import Attribute as Attribute
|
|
16
|
+
from .models import Model as Model
|
|
17
|
+
from .models import Schema as Schema
|
|
18
|
+
from .models import configure as configure
|
|
19
|
+
from .models import get_collection as get_collection
|
|
20
|
+
from .models import get_schema as get_schema
|
|
21
|
+
from .pipelines import Pipeline as Pipeline
|
|
22
|
+
from .predicates import Field as Field
|