mongo-entity-orm 0.1.0__tar.gz → 0.1.2__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.
- mongo_entity_orm-0.1.2/PKG-INFO +261 -0
- mongo_entity_orm-0.1.2/README.md +236 -0
- {mongo_entity_orm-0.1.0 → mongo_entity_orm-0.1.2}/pyproject.toml +4 -4
- {mongo_entity_orm-0.1.0 → mongo_entity_orm-0.1.2}/src/mongo_orm/core.py +60 -6
- mongo_entity_orm-0.1.2/src/mongo_orm/utils.py +137 -0
- mongo_entity_orm-0.1.0/PKG-INFO +0 -55
- mongo_entity_orm-0.1.0/README.md +0 -30
- {mongo_entity_orm-0.1.0 → mongo_entity_orm-0.1.2}/LICENSE +0 -0
- {mongo_entity_orm-0.1.0 → mongo_entity_orm-0.1.2}/src/mongo_orm/__init__.py +0 -0
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: mongo-entity-orm
|
|
3
|
+
Version: 0.1.2
|
|
4
|
+
Summary: A MongoDB ORM for Python
|
|
5
|
+
License: MIT
|
|
6
|
+
Keywords: mongodb,orm,mongo,async
|
|
7
|
+
Author: Juraj Bezdek
|
|
8
|
+
Author-email: juraj.bezdek@gmail.com
|
|
9
|
+
Requires-Python: >=3.9,<4.0
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
19
|
+
Requires-Dist: motor (>=3.7.1,<4.0.0)
|
|
20
|
+
Requires-Dist: pymongo (>=4.13.2,<5.0.0)
|
|
21
|
+
Project-URL: Documentation, https://github.com/ju-bezdek/mongo-orm#readme
|
|
22
|
+
Project-URL: Homepage, https://github.com/ju-bezdek/mongo-orm
|
|
23
|
+
Project-URL: Repository, https://github.com/ju-bezdek/mongo-orm
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
|
|
26
|
+
# mongo-orm
|
|
27
|
+
|
|
28
|
+
A MongoDB ORM for Python
|
|
29
|
+
|
|
30
|
+
## Installation
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
pip install mongo-entity-orm
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
or
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
uv add mongo-entity-orm
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Configuration
|
|
43
|
+
|
|
44
|
+
Set the following environment variables before using the ORM:
|
|
45
|
+
|
|
46
|
+
| Variable | Description |
|
|
47
|
+
|---|---|
|
|
48
|
+
| `MONGODB_URI` | MongoDB connection string |
|
|
49
|
+
| `MONGO_DATABASE_NAME` | Name of the database |
|
|
50
|
+
|
|
51
|
+
## Usage
|
|
52
|
+
|
|
53
|
+
### Defining an Entity
|
|
54
|
+
|
|
55
|
+
Use the `@entity` decorator to map a class to a MongoDB collection. Your class should inherit from `BaseEntity`.
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
from mongo_orm import BaseEntity, entity
|
|
59
|
+
|
|
60
|
+
@entity(collection_name="users")
|
|
61
|
+
class User(BaseEntity):
|
|
62
|
+
name: str
|
|
63
|
+
email: str
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Every entity automatically has an `id` field (UUID string) and a `tenant_id` field (defaults to `"-"`).
|
|
67
|
+
|
|
68
|
+
### CRUD Operations
|
|
69
|
+
|
|
70
|
+
All operations are available in both async (`a`-prefixed) and sync variants.
|
|
71
|
+
|
|
72
|
+
#### Get by ID
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
# async
|
|
76
|
+
user = await User.aget(id=user_id, tenant_id=tenant_id)
|
|
77
|
+
|
|
78
|
+
# sync
|
|
79
|
+
user = User.get(id=user_id, tenant_id=tenant_id)
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Pass `raise_not_found=True` to raise an exception instead of returning `None` when the entity is not found.
|
|
83
|
+
|
|
84
|
+
#### Save
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
user = User(name="Alice", email="alice@example.com", tenant_id="acme")
|
|
88
|
+
|
|
89
|
+
# async
|
|
90
|
+
await user.asave()
|
|
91
|
+
|
|
92
|
+
# sync
|
|
93
|
+
user.save()
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
`save()` / `asave()` performs an upsert based on `id`, so it works for both creating and updating.
|
|
97
|
+
|
|
98
|
+
#### Update fields
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
# Update in-memory only
|
|
102
|
+
user.update(name="Bob")
|
|
103
|
+
|
|
104
|
+
# Update and immediately save (async)
|
|
105
|
+
await user.aupdate(name="Bob", auto_save=True)
|
|
106
|
+
|
|
107
|
+
# Update and immediately save (sync)
|
|
108
|
+
user.update(name="Bob", auto_save=True)
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
#### Delete
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
# Delete an entity instance
|
|
115
|
+
user.delete()
|
|
116
|
+
|
|
117
|
+
# Delete by ID
|
|
118
|
+
User.delete_by_id(id=user_id, tenant_id=tenant_id)
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Pass `ignore_not_found=True` to suppress an exception when the entity does not exist.
|
|
122
|
+
|
|
123
|
+
### Querying
|
|
124
|
+
|
|
125
|
+
#### Find (filter)
|
|
126
|
+
|
|
127
|
+
`filter` accepts a standard MongoDB query dict.
|
|
128
|
+
|
|
129
|
+
```python
|
|
130
|
+
# async
|
|
131
|
+
users = await User.afind(tenant_id=tenant_id, filter={"name": "Alice"})
|
|
132
|
+
|
|
133
|
+
# sync
|
|
134
|
+
users = User.find(tenant_id=tenant_id, filter={"name": "Alice"})
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
MongoDB operators work as expected:
|
|
138
|
+
|
|
139
|
+
```python
|
|
140
|
+
users = await User.afind(
|
|
141
|
+
tenant_id=tenant_id,
|
|
142
|
+
filter={"name": {"$in": ["Alice", "Bob"]}, "email": {"$regex": "@example.com$"}},
|
|
143
|
+
)
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Use `tenant_id="*"` to query across all tenants.
|
|
147
|
+
|
|
148
|
+
#### Pagination
|
|
149
|
+
|
|
150
|
+
```python
|
|
151
|
+
users = await User.afind(tenant_id=tenant_id, filter={}, skip=0, limit=20)
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
#### Sorting
|
|
155
|
+
|
|
156
|
+
```python
|
|
157
|
+
# Ascending
|
|
158
|
+
users = await User.afind(tenant_id=tenant_id, order_by="name")
|
|
159
|
+
|
|
160
|
+
# Descending (prefix with "-")
|
|
161
|
+
users = await User.afind(tenant_id=tenant_id, order_by="-name")
|
|
162
|
+
|
|
163
|
+
# Multiple sort fields
|
|
164
|
+
users = await User.afind(tenant_id=tenant_id, order_by=["-created_at", "name"])
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
#### Find first
|
|
168
|
+
|
|
169
|
+
```python
|
|
170
|
+
user = await User.afind_first(tenant_id=tenant_id, filter={"email": "alice@example.com"})
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Returns `None` if no match is found.
|
|
174
|
+
|
|
175
|
+
#### Count
|
|
176
|
+
|
|
177
|
+
```python
|
|
178
|
+
# async
|
|
179
|
+
total = await User.acount(tenant_id=tenant_id, filter={"name": "Alice"})
|
|
180
|
+
|
|
181
|
+
# sync
|
|
182
|
+
total = User.count(tenant_id=tenant_id, filter={"name": "Alice"})
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
#### Scroll pages (batch iteration)
|
|
186
|
+
|
|
187
|
+
```python
|
|
188
|
+
# async
|
|
189
|
+
async for page in User.ascroll_pages(tenant_id=tenant_id, page_size=50):
|
|
190
|
+
for user in page:
|
|
191
|
+
...
|
|
192
|
+
|
|
193
|
+
# sync
|
|
194
|
+
for page in User.scroll_pages(tenant_id=tenant_id, page_size=50):
|
|
195
|
+
for user in page:
|
|
196
|
+
...
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
#### Bulk save
|
|
200
|
+
|
|
201
|
+
```python
|
|
202
|
+
await User.bulk_save([user1, user2, user3])
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
### Index Management
|
|
206
|
+
|
|
207
|
+
Define indexes on the entity class using `__indexes__`:
|
|
208
|
+
|
|
209
|
+
```python
|
|
210
|
+
@entity(collection_name="users")
|
|
211
|
+
class User(BaseEntity):
|
|
212
|
+
name: str
|
|
213
|
+
email: str
|
|
214
|
+
|
|
215
|
+
__indexes__ = [
|
|
216
|
+
{"keys": [("email", 1)], "unique": True},
|
|
217
|
+
{"keys": [("name", 1)]},
|
|
218
|
+
{"keys": [("tenant_id", 1), ("name", 1)]},
|
|
219
|
+
]
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
Index management is controlled by the `MONGODB_INDEX_AUTOAPPLY` environment variable:
|
|
223
|
+
|
|
224
|
+
| Value | Behaviour |
|
|
225
|
+
|---|---|
|
|
226
|
+
| `never` (default) | No automatic index management |
|
|
227
|
+
| `always` | Checks and applies indexes on every startup |
|
|
228
|
+
| `auto-lock` | Applies indexes once; writes a hash to `mongo-orm.lock` and skips on subsequent startups |
|
|
229
|
+
|
|
230
|
+
#### Manual application
|
|
231
|
+
|
|
232
|
+
```python
|
|
233
|
+
from mongo_orm.utils import apply_all_indexes
|
|
234
|
+
|
|
235
|
+
# Respects MONGODB_INDEX_AUTOAPPLY
|
|
236
|
+
apply_all_indexes()
|
|
237
|
+
|
|
238
|
+
# Force a specific mode
|
|
239
|
+
apply_all_indexes(mode="always")
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
## Development
|
|
243
|
+
|
|
244
|
+
```bash
|
|
245
|
+
# Install dependencies
|
|
246
|
+
poetry install
|
|
247
|
+
|
|
248
|
+
# Run tests
|
|
249
|
+
poetry run pytest
|
|
250
|
+
|
|
251
|
+
# Format code
|
|
252
|
+
poetry run black .
|
|
253
|
+
poetry run isort .
|
|
254
|
+
|
|
255
|
+
# Type checking
|
|
256
|
+
poetry run mypy src/
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
## License
|
|
260
|
+
|
|
261
|
+
MIT
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
# mongo-orm
|
|
2
|
+
|
|
3
|
+
A MongoDB ORM for Python
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
pip install mongo-entity-orm
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
or
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
uv add mongo-entity-orm
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Configuration
|
|
18
|
+
|
|
19
|
+
Set the following environment variables before using the ORM:
|
|
20
|
+
|
|
21
|
+
| Variable | Description |
|
|
22
|
+
|---|---|
|
|
23
|
+
| `MONGODB_URI` | MongoDB connection string |
|
|
24
|
+
| `MONGO_DATABASE_NAME` | Name of the database |
|
|
25
|
+
|
|
26
|
+
## Usage
|
|
27
|
+
|
|
28
|
+
### Defining an Entity
|
|
29
|
+
|
|
30
|
+
Use the `@entity` decorator to map a class to a MongoDB collection. Your class should inherit from `BaseEntity`.
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
from mongo_orm import BaseEntity, entity
|
|
34
|
+
|
|
35
|
+
@entity(collection_name="users")
|
|
36
|
+
class User(BaseEntity):
|
|
37
|
+
name: str
|
|
38
|
+
email: str
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Every entity automatically has an `id` field (UUID string) and a `tenant_id` field (defaults to `"-"`).
|
|
42
|
+
|
|
43
|
+
### CRUD Operations
|
|
44
|
+
|
|
45
|
+
All operations are available in both async (`a`-prefixed) and sync variants.
|
|
46
|
+
|
|
47
|
+
#### Get by ID
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
# async
|
|
51
|
+
user = await User.aget(id=user_id, tenant_id=tenant_id)
|
|
52
|
+
|
|
53
|
+
# sync
|
|
54
|
+
user = User.get(id=user_id, tenant_id=tenant_id)
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Pass `raise_not_found=True` to raise an exception instead of returning `None` when the entity is not found.
|
|
58
|
+
|
|
59
|
+
#### Save
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
user = User(name="Alice", email="alice@example.com", tenant_id="acme")
|
|
63
|
+
|
|
64
|
+
# async
|
|
65
|
+
await user.asave()
|
|
66
|
+
|
|
67
|
+
# sync
|
|
68
|
+
user.save()
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
`save()` / `asave()` performs an upsert based on `id`, so it works for both creating and updating.
|
|
72
|
+
|
|
73
|
+
#### Update fields
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
# Update in-memory only
|
|
77
|
+
user.update(name="Bob")
|
|
78
|
+
|
|
79
|
+
# Update and immediately save (async)
|
|
80
|
+
await user.aupdate(name="Bob", auto_save=True)
|
|
81
|
+
|
|
82
|
+
# Update and immediately save (sync)
|
|
83
|
+
user.update(name="Bob", auto_save=True)
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
#### Delete
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
# Delete an entity instance
|
|
90
|
+
user.delete()
|
|
91
|
+
|
|
92
|
+
# Delete by ID
|
|
93
|
+
User.delete_by_id(id=user_id, tenant_id=tenant_id)
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Pass `ignore_not_found=True` to suppress an exception when the entity does not exist.
|
|
97
|
+
|
|
98
|
+
### Querying
|
|
99
|
+
|
|
100
|
+
#### Find (filter)
|
|
101
|
+
|
|
102
|
+
`filter` accepts a standard MongoDB query dict.
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
# async
|
|
106
|
+
users = await User.afind(tenant_id=tenant_id, filter={"name": "Alice"})
|
|
107
|
+
|
|
108
|
+
# sync
|
|
109
|
+
users = User.find(tenant_id=tenant_id, filter={"name": "Alice"})
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
MongoDB operators work as expected:
|
|
113
|
+
|
|
114
|
+
```python
|
|
115
|
+
users = await User.afind(
|
|
116
|
+
tenant_id=tenant_id,
|
|
117
|
+
filter={"name": {"$in": ["Alice", "Bob"]}, "email": {"$regex": "@example.com$"}},
|
|
118
|
+
)
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Use `tenant_id="*"` to query across all tenants.
|
|
122
|
+
|
|
123
|
+
#### Pagination
|
|
124
|
+
|
|
125
|
+
```python
|
|
126
|
+
users = await User.afind(tenant_id=tenant_id, filter={}, skip=0, limit=20)
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
#### Sorting
|
|
130
|
+
|
|
131
|
+
```python
|
|
132
|
+
# Ascending
|
|
133
|
+
users = await User.afind(tenant_id=tenant_id, order_by="name")
|
|
134
|
+
|
|
135
|
+
# Descending (prefix with "-")
|
|
136
|
+
users = await User.afind(tenant_id=tenant_id, order_by="-name")
|
|
137
|
+
|
|
138
|
+
# Multiple sort fields
|
|
139
|
+
users = await User.afind(tenant_id=tenant_id, order_by=["-created_at", "name"])
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
#### Find first
|
|
143
|
+
|
|
144
|
+
```python
|
|
145
|
+
user = await User.afind_first(tenant_id=tenant_id, filter={"email": "alice@example.com"})
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Returns `None` if no match is found.
|
|
149
|
+
|
|
150
|
+
#### Count
|
|
151
|
+
|
|
152
|
+
```python
|
|
153
|
+
# async
|
|
154
|
+
total = await User.acount(tenant_id=tenant_id, filter={"name": "Alice"})
|
|
155
|
+
|
|
156
|
+
# sync
|
|
157
|
+
total = User.count(tenant_id=tenant_id, filter={"name": "Alice"})
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
#### Scroll pages (batch iteration)
|
|
161
|
+
|
|
162
|
+
```python
|
|
163
|
+
# async
|
|
164
|
+
async for page in User.ascroll_pages(tenant_id=tenant_id, page_size=50):
|
|
165
|
+
for user in page:
|
|
166
|
+
...
|
|
167
|
+
|
|
168
|
+
# sync
|
|
169
|
+
for page in User.scroll_pages(tenant_id=tenant_id, page_size=50):
|
|
170
|
+
for user in page:
|
|
171
|
+
...
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
#### Bulk save
|
|
175
|
+
|
|
176
|
+
```python
|
|
177
|
+
await User.bulk_save([user1, user2, user3])
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### Index Management
|
|
181
|
+
|
|
182
|
+
Define indexes on the entity class using `__indexes__`:
|
|
183
|
+
|
|
184
|
+
```python
|
|
185
|
+
@entity(collection_name="users")
|
|
186
|
+
class User(BaseEntity):
|
|
187
|
+
name: str
|
|
188
|
+
email: str
|
|
189
|
+
|
|
190
|
+
__indexes__ = [
|
|
191
|
+
{"keys": [("email", 1)], "unique": True},
|
|
192
|
+
{"keys": [("name", 1)]},
|
|
193
|
+
{"keys": [("tenant_id", 1), ("name", 1)]},
|
|
194
|
+
]
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Index management is controlled by the `MONGODB_INDEX_AUTOAPPLY` environment variable:
|
|
198
|
+
|
|
199
|
+
| Value | Behaviour |
|
|
200
|
+
|---|---|
|
|
201
|
+
| `never` (default) | No automatic index management |
|
|
202
|
+
| `always` | Checks and applies indexes on every startup |
|
|
203
|
+
| `auto-lock` | Applies indexes once; writes a hash to `mongo-orm.lock` and skips on subsequent startups |
|
|
204
|
+
|
|
205
|
+
#### Manual application
|
|
206
|
+
|
|
207
|
+
```python
|
|
208
|
+
from mongo_orm.utils import apply_all_indexes
|
|
209
|
+
|
|
210
|
+
# Respects MONGODB_INDEX_AUTOAPPLY
|
|
211
|
+
apply_all_indexes()
|
|
212
|
+
|
|
213
|
+
# Force a specific mode
|
|
214
|
+
apply_all_indexes(mode="always")
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
## Development
|
|
218
|
+
|
|
219
|
+
```bash
|
|
220
|
+
# Install dependencies
|
|
221
|
+
poetry install
|
|
222
|
+
|
|
223
|
+
# Run tests
|
|
224
|
+
poetry run pytest
|
|
225
|
+
|
|
226
|
+
# Format code
|
|
227
|
+
poetry run black .
|
|
228
|
+
poetry run isort .
|
|
229
|
+
|
|
230
|
+
# Type checking
|
|
231
|
+
poetry run mypy src/
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
## License
|
|
235
|
+
|
|
236
|
+
MIT
|
|
@@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
|
|
|
4
4
|
|
|
5
5
|
[tool.poetry]
|
|
6
6
|
name = "mongo-entity-orm"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.2"
|
|
8
8
|
description = "A MongoDB ORM for Python"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
authors = ["Juraj Bezdek <juraj.bezdek@gmail.com>"]
|
|
@@ -12,7 +12,7 @@ license = "MIT"
|
|
|
12
12
|
packages = [{include = "mongo_orm", from = "src"}]
|
|
13
13
|
keywords = ["mongodb", "orm", "mongo", "async"]
|
|
14
14
|
homepage = "https://github.com/ju-bezdek/mongo-orm"
|
|
15
|
-
repository = "https://github.com/ju-bezdek/mongo"
|
|
15
|
+
repository = "https://github.com/ju-bezdek/mongo-orm"
|
|
16
16
|
|
|
17
17
|
classifiers = [
|
|
18
18
|
"Programming Language :: Python :: 3",
|
|
@@ -25,8 +25,8 @@ classifiers = [
|
|
|
25
25
|
]
|
|
26
26
|
|
|
27
27
|
[tool.poetry.urls]
|
|
28
|
-
Repository = "https://github.com/ju-bezdek/mongo"
|
|
29
|
-
Documentation = "https://github.com/ju-bezdek/mongo#readme"
|
|
28
|
+
Repository = "https://github.com/ju-bezdek/mongo-orm"
|
|
29
|
+
Documentation = "https://github.com/ju-bezdek/mongo-orm#readme"
|
|
30
30
|
|
|
31
31
|
[tool.poetry.dependencies]
|
|
32
32
|
python = ">=3.9,<4.0"
|
|
@@ -6,15 +6,20 @@ import logging
|
|
|
6
6
|
from collections import deque
|
|
7
7
|
import os
|
|
8
8
|
from typing import (
|
|
9
|
+
Any,
|
|
9
10
|
AsyncIterator,
|
|
10
11
|
Awaitable,
|
|
11
12
|
Callable,
|
|
13
|
+
Dict,
|
|
12
14
|
Generator,
|
|
13
15
|
Iterable,
|
|
14
16
|
List,
|
|
17
|
+
Optional,
|
|
18
|
+
Tuple,
|
|
15
19
|
Type,
|
|
16
20
|
TypeVar,
|
|
17
21
|
TypedDict,
|
|
22
|
+
Union,
|
|
18
23
|
)
|
|
19
24
|
from uuid import NAMESPACE_URL, uuid4, uuid5
|
|
20
25
|
from motor.motor_asyncio import AsyncIOMotorClient, AsyncIOMotorCollection
|
|
@@ -34,6 +39,10 @@ class Config:
|
|
|
34
39
|
def MONGO_DATABASE_NAME():
|
|
35
40
|
return os.environ.get("MONGO_DATABASE_NAME", None)
|
|
36
41
|
|
|
42
|
+
@staticmethod
|
|
43
|
+
def MONGODB_INDEX_AUTOAPPLY():
|
|
44
|
+
return os.environ.get("MONGODB_INDEX_AUTOAPPLY", "never")
|
|
45
|
+
|
|
37
46
|
|
|
38
47
|
def encode_tenant_id(tenant_id, id):
|
|
39
48
|
return str(uuid5(NAMESPACE_URL, f"{id}/{tenant_id}"))
|
|
@@ -45,29 +54,65 @@ class SaveResult(TypedDict):
|
|
|
45
54
|
|
|
46
55
|
|
|
47
56
|
class Repository:
|
|
57
|
+
all_entities: List[Type["BaseEntity"]] = []
|
|
48
58
|
|
|
49
59
|
def __init__(self, mongo_db: MongoDatabase = None) -> None:
|
|
50
60
|
if mongo_db:
|
|
51
61
|
self.mongo_db = mongo_db
|
|
52
62
|
else:
|
|
63
|
+
mongodb_uri = Config.MONGODB_URI()
|
|
64
|
+
mongo_db_name = Config.MONGO_DATABASE_NAME()
|
|
65
|
+
if not mongodb_uri and not mongo_db_name:
|
|
66
|
+
raise ValueError(
|
|
67
|
+
"MONGODB_URI and MONGO_DATABASE_NAME environment variables are not set"
|
|
68
|
+
)
|
|
69
|
+
if not mongodb_uri:
|
|
70
|
+
raise ValueError("MONGODB_URI environment variable is not set")
|
|
71
|
+
if not mongo_db_name:
|
|
72
|
+
raise ValueError("MONGO_DATABASE_NAME environment variable is not set")
|
|
53
73
|
|
|
54
74
|
self.mongo_client = MongoClient(
|
|
55
|
-
|
|
75
|
+
mongodb_uri, tlsAllowInvalidCertificates=True
|
|
56
76
|
)
|
|
57
77
|
self.async_mongo_client = AsyncIOMotorClient(
|
|
58
|
-
|
|
78
|
+
mongodb_uri, tls=True, tlsAllowInvalidCertificates=True
|
|
59
79
|
)
|
|
60
80
|
self.mongo_client.admin.command("ping")
|
|
61
|
-
self.mongo_db = self.mongo_client[str(
|
|
62
|
-
self.async_mongo_db = self.async_mongo_client[
|
|
63
|
-
str(Config.MONGO_DATABASE_NAME())
|
|
64
|
-
]
|
|
81
|
+
self.mongo_db = self.mongo_client[str(mongo_db_name)]
|
|
82
|
+
self.async_mongo_db = self.async_mongo_client[str(mongo_db_name)]
|
|
65
83
|
|
|
66
84
|
|
|
67
85
|
REPOSITORY = Repository()
|
|
68
86
|
|
|
69
87
|
|
|
70
88
|
T = TypeVar("T", bound="BaseEntity")
|
|
89
|
+
Keys = Union[
|
|
90
|
+
str,
|
|
91
|
+
List[Union[str, Tuple[str, int]]],
|
|
92
|
+
Dict[str, int],
|
|
93
|
+
]
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class IndexSpec(TypedDict, total=False):
|
|
97
|
+
"""
|
|
98
|
+
Typed dict describing a MongoDB index.
|
|
99
|
+
|
|
100
|
+
Examples:
|
|
101
|
+
{"keys": "email", "unique": True}
|
|
102
|
+
{"keys": [("tenant_id", 1), ("created_at", -1)], "name": "tenant_created_idx"}
|
|
103
|
+
{"keys": {"email": 1}, "unique": True}
|
|
104
|
+
"""
|
|
105
|
+
|
|
106
|
+
keys: Keys
|
|
107
|
+
# Optional index options
|
|
108
|
+
unique: bool
|
|
109
|
+
sparse: bool
|
|
110
|
+
background: bool
|
|
111
|
+
name: str
|
|
112
|
+
expireAfterSeconds: int
|
|
113
|
+
expire_after_seconds: int # alias
|
|
114
|
+
partialFilterExpression: Dict[str, Any]
|
|
115
|
+
weights: Dict[str, int]
|
|
71
116
|
|
|
72
117
|
|
|
73
118
|
class BaseEntity(BaseModel):
|
|
@@ -81,6 +126,8 @@ class BaseEntity(BaseModel):
|
|
|
81
126
|
_loaded_at: datetime.datetime | None = PrivateAttr(None)
|
|
82
127
|
_tracked_fields_state: dict | None = PrivateAttr(None)
|
|
83
128
|
|
|
129
|
+
__indexes__: Optional[List[IndexSpec]] = None
|
|
130
|
+
|
|
84
131
|
def __init__(self, **data) -> None:
|
|
85
132
|
if not hasattr(self.__class__, "collection"):
|
|
86
133
|
raise Exception(
|
|
@@ -557,6 +604,13 @@ def entity(
|
|
|
557
604
|
|
|
558
605
|
cls.version_field = version_field
|
|
559
606
|
|
|
607
|
+
if cls not in REPOSITORY.all_entities:
|
|
608
|
+
REPOSITORY.all_entities.append(cls)
|
|
609
|
+
|
|
610
|
+
from .utils import apply_indexes
|
|
611
|
+
|
|
612
|
+
apply_indexes(cls)
|
|
613
|
+
|
|
560
614
|
return cls
|
|
561
615
|
|
|
562
616
|
return decorator
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
import logging
|
|
3
|
+
import os
|
|
4
|
+
from typing import TYPE_CHECKING, List, Literal, Type
|
|
5
|
+
from motor.motor_asyncio import AsyncIOMotorClient, AsyncIOMotorCollection
|
|
6
|
+
from pymongo.collection import Collection
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from .core import BaseEntity, IndexSpec
|
|
10
|
+
|
|
11
|
+
LOCK_FILE = "mongo-orm.lock"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _get_index_hash(collection_name: str, spec: "IndexSpec") -> str:
|
|
15
|
+
"""Generate a stable hash for a collection + index specification."""
|
|
16
|
+
keys = spec.get("keys") or spec.get("key")
|
|
17
|
+
# Normalize keys to a stable tuple representation
|
|
18
|
+
if isinstance(keys, dict):
|
|
19
|
+
# Keep order as provided in dict
|
|
20
|
+
norm_keys = tuple((str(k), float(v)) for k, v in keys.items())
|
|
21
|
+
elif isinstance(keys, list):
|
|
22
|
+
norm_keys = tuple((str(k), float(v)) for k, v in keys)
|
|
23
|
+
else:
|
|
24
|
+
norm_keys = ((str(keys), 1.0),)
|
|
25
|
+
|
|
26
|
+
# Create a stable representation of the spec including important options
|
|
27
|
+
relevant_parts = {
|
|
28
|
+
"collection": collection_name,
|
|
29
|
+
"keys": norm_keys,
|
|
30
|
+
"unique": spec.get("unique", False),
|
|
31
|
+
"sparse": spec.get("sparse", False),
|
|
32
|
+
"partialFilterExpression": spec.get("partialFilterExpression"),
|
|
33
|
+
"expireAfterSeconds": spec.get("expireAfterSeconds")
|
|
34
|
+
or spec.get("expire_after_seconds"),
|
|
35
|
+
}
|
|
36
|
+
# Sort top-level keys of relevant_parts to ensure stable string representation
|
|
37
|
+
spec_str = str(sorted(relevant_parts.items(), key=lambda x: x[0]))
|
|
38
|
+
return hashlib.sha256(spec_str.encode()).hexdigest()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def index_exists(collection: Collection, spec: "IndexSpec") -> bool:
|
|
42
|
+
"""Return True if an equivalent index already exists in `collection` (sync)."""
|
|
43
|
+
|
|
44
|
+
# Normalize target keys to a list of tuples
|
|
45
|
+
keys = spec.get("keys") or spec.get("key")
|
|
46
|
+
if isinstance(keys, dict):
|
|
47
|
+
target_key = list(keys.items())
|
|
48
|
+
elif isinstance(keys, list):
|
|
49
|
+
target_key = keys
|
|
50
|
+
else:
|
|
51
|
+
target_key = [(keys, 1)]
|
|
52
|
+
|
|
53
|
+
# Normalize values to floats for comparison
|
|
54
|
+
target_key = [(str(k), float(v)) for k, v in target_key]
|
|
55
|
+
|
|
56
|
+
existing_indexes = collection.index_information()
|
|
57
|
+
|
|
58
|
+
for name, info in existing_indexes.items():
|
|
59
|
+
# info['key'] can be a list of tuples or a SON/dict object
|
|
60
|
+
raw_existing_key = info["key"]
|
|
61
|
+
if hasattr(raw_existing_key, "items"):
|
|
62
|
+
existing_key = [(str(k), float(v)) for k, v in raw_existing_key.items()]
|
|
63
|
+
else:
|
|
64
|
+
existing_key = [(str(k), float(v)) for k, v in raw_existing_key]
|
|
65
|
+
|
|
66
|
+
if existing_key == target_key:
|
|
67
|
+
return True
|
|
68
|
+
return False
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def apply_indexes(cls: Type["BaseEntity"], mode: Literal["auto-lock", "always"] = None):
|
|
72
|
+
"""Check and apply indexes for a specific entity class."""
|
|
73
|
+
from .core import Config
|
|
74
|
+
|
|
75
|
+
mode = mode or Config.MONGODB_INDEX_AUTOAPPLY()
|
|
76
|
+
if not mode or mode == "never":
|
|
77
|
+
return
|
|
78
|
+
|
|
79
|
+
indexes = getattr(cls, "__indexes__", None)
|
|
80
|
+
if not indexes:
|
|
81
|
+
return
|
|
82
|
+
|
|
83
|
+
collection = cls.get_collection()
|
|
84
|
+
collection_name = cls.__collection_name__
|
|
85
|
+
|
|
86
|
+
applied_hashes = set()
|
|
87
|
+
if mode == "auto-lock" and os.path.exists(LOCK_FILE):
|
|
88
|
+
with open(LOCK_FILE, "r") as f:
|
|
89
|
+
applied_hashes = {line.strip() for line in f if line.strip()}
|
|
90
|
+
|
|
91
|
+
new_hashes = []
|
|
92
|
+
|
|
93
|
+
for spec in indexes:
|
|
94
|
+
idx_hash = _get_index_hash(collection_name, spec)
|
|
95
|
+
|
|
96
|
+
if mode == "auto-lock" and idx_hash in applied_hashes:
|
|
97
|
+
continue
|
|
98
|
+
|
|
99
|
+
if not index_exists(collection, spec):
|
|
100
|
+
# Create index
|
|
101
|
+
keys = spec.get("keys") or spec.get("key")
|
|
102
|
+
options = {
|
|
103
|
+
k: v
|
|
104
|
+
for k, v in spec.items()
|
|
105
|
+
if k not in ("keys", "key", "expire_after_seconds")
|
|
106
|
+
}
|
|
107
|
+
if "expire_after_seconds" in spec:
|
|
108
|
+
options["expireAfterSeconds"] = spec["expire_after_seconds"]
|
|
109
|
+
|
|
110
|
+
try:
|
|
111
|
+
collection.create_index(keys, **options)
|
|
112
|
+
except Exception as e:
|
|
113
|
+
logging.error(
|
|
114
|
+
f"Failed to create index {keys} on {collection_name}: {e}"
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
if mode == "auto-lock":
|
|
118
|
+
new_hashes.append(idx_hash)
|
|
119
|
+
|
|
120
|
+
if new_hashes:
|
|
121
|
+
with open(LOCK_FILE, "a") as f:
|
|
122
|
+
for h in new_hashes:
|
|
123
|
+
f.write(f"{h}\n")
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def apply_all_indexes(mode: str = None):
|
|
127
|
+
"""Apply indexes for all registered entity classes."""
|
|
128
|
+
from .core import Config, Repository
|
|
129
|
+
|
|
130
|
+
if mode is None:
|
|
131
|
+
mode = Config.MONGODB_INDEX_AUTOAPPLY()
|
|
132
|
+
|
|
133
|
+
if not mode or mode == "never":
|
|
134
|
+
return
|
|
135
|
+
|
|
136
|
+
for cls in Repository.all_entities:
|
|
137
|
+
apply_indexes(cls, mode)
|
mongo_entity_orm-0.1.0/PKG-INFO
DELETED
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.3
|
|
2
|
-
Name: mongo-entity-orm
|
|
3
|
-
Version: 0.1.0
|
|
4
|
-
Summary: A MongoDB ORM for Python
|
|
5
|
-
License: MIT
|
|
6
|
-
Keywords: mongodb,orm,mongo,async
|
|
7
|
-
Author: Juraj Bezdek
|
|
8
|
-
Author-email: juraj.bezdek@gmail.com
|
|
9
|
-
Requires-Python: >=3.9,<4.0
|
|
10
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
-
Classifier: Operating System :: OS Independent
|
|
12
|
-
Classifier: Programming Language :: Python :: 3
|
|
13
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
14
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
-
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
-
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
-
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
-
Classifier: Programming Language :: Python :: 3 :: Only
|
|
19
|
-
Requires-Dist: motor (>=3.7.1,<4.0.0)
|
|
20
|
-
Requires-Dist: pymongo (>=4.13.2,<5.0.0)
|
|
21
|
-
Project-URL: Documentation, https://github.com/ju-bezdek/mongo#readme
|
|
22
|
-
Project-URL: Homepage, https://github.com/ju-bezdek/mongo-orm
|
|
23
|
-
Project-URL: Repository, https://github.com/ju-bezdek/mongo
|
|
24
|
-
Description-Content-Type: text/markdown
|
|
25
|
-
|
|
26
|
-
# mongo-orm
|
|
27
|
-
|
|
28
|
-
A MongoDB ORM for Python
|
|
29
|
-
|
|
30
|
-
## Installation
|
|
31
|
-
|
|
32
|
-
```bash
|
|
33
|
-
pip install mongo-orm
|
|
34
|
-
```
|
|
35
|
-
|
|
36
|
-
## Development
|
|
37
|
-
|
|
38
|
-
```bash
|
|
39
|
-
# Install dependencies
|
|
40
|
-
poetry install
|
|
41
|
-
|
|
42
|
-
# Run tests
|
|
43
|
-
poetry run pytest
|
|
44
|
-
|
|
45
|
-
# Format code
|
|
46
|
-
poetry run black .
|
|
47
|
-
poetry run isort .
|
|
48
|
-
|
|
49
|
-
# Type checking
|
|
50
|
-
poetry run mypy src/
|
|
51
|
-
```
|
|
52
|
-
|
|
53
|
-
## License
|
|
54
|
-
|
|
55
|
-
MIT
|
mongo_entity_orm-0.1.0/README.md
DELETED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
# mongo-orm
|
|
2
|
-
|
|
3
|
-
A MongoDB ORM for Python
|
|
4
|
-
|
|
5
|
-
## Installation
|
|
6
|
-
|
|
7
|
-
```bash
|
|
8
|
-
pip install mongo-orm
|
|
9
|
-
```
|
|
10
|
-
|
|
11
|
-
## Development
|
|
12
|
-
|
|
13
|
-
```bash
|
|
14
|
-
# Install dependencies
|
|
15
|
-
poetry install
|
|
16
|
-
|
|
17
|
-
# Run tests
|
|
18
|
-
poetry run pytest
|
|
19
|
-
|
|
20
|
-
# Format code
|
|
21
|
-
poetry run black .
|
|
22
|
-
poetry run isort .
|
|
23
|
-
|
|
24
|
-
# Type checking
|
|
25
|
-
poetry run mypy src/
|
|
26
|
-
```
|
|
27
|
-
|
|
28
|
-
## License
|
|
29
|
-
|
|
30
|
-
MIT
|
|
File without changes
|
|
File without changes
|