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.
@@ -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.0"
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
- Config.MONGODB_URI(), tlsAllowInvalidCertificates=True
75
+ mongodb_uri, tlsAllowInvalidCertificates=True
56
76
  )
57
77
  self.async_mongo_client = AsyncIOMotorClient(
58
- Config.MONGODB_URI(), tls=True, tlsAllowInvalidCertificates=True
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(Config.MONGO_DATABASE_NAME())]
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)
@@ -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
@@ -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