mongo-entity-orm 0.1.0__tar.gz → 0.1.1__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,94 @@
1
+ Metadata-Version: 2.3
2
+ Name: mongo-entity-orm
3
+ Version: 0.1.1
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
+ ## Usage
31
+
32
+ ### Defining an Entity
33
+
34
+ Use the `@entity` decorator to map a class to a MongoDB collection. Your class should inherit from `BaseEntity`.
35
+
36
+ ```python
37
+ from mongo_orm import BaseEntity, entity
38
+
39
+ @entity("users")
40
+ class User(BaseEntity):
41
+ name: str
42
+ email: str
43
+
44
+ # Define indexes
45
+ __indexes__ = [
46
+ {"keys": [("email", 1)], "unique": True},
47
+ {"keys": [("name", 1)]}
48
+ ]
49
+ ```
50
+
51
+ ### Index Management
52
+
53
+ The ORM can automatically manage your MongoDB indexes based on the `__indexes__` attribute. This behavior is controlled by the `MONGODB_INDEX_AUTOAPPLY` environment variable.
54
+
55
+ #### Configuration (`MONGODB_INDEX_AUTOAPPLY`)
56
+
57
+ - `never` (default): No automatic index management.
58
+ - `always`: Checks and applies indexes every time an entity class is initialized (on startup).
59
+ - `auto-lock`: Checks and applies indexes once. After a successful check, it writes a hash of the index to `mongo-orm.lock`. Subsequent startups will skip the check if the hash is present in the lock file.
60
+
61
+ #### Manual Index Application
62
+
63
+ You can also trigger index application manually for all registered entities:
64
+
65
+ ```python
66
+ from mongo_orm.utils import apply_all_indexes
67
+
68
+ # Apply indexes using the current environment configuration
69
+ apply_all_indexes()
70
+
71
+ # Or force a specific mode
72
+ apply_all_indexes(mode="always")
73
+ ```
74
+
75
+ ## Development
76
+
77
+ ```bash
78
+ # Install dependencies
79
+ poetry install
80
+
81
+ # Run tests
82
+ poetry run pytest
83
+
84
+ # Format code
85
+ poetry run black .
86
+ poetry run isort .
87
+
88
+ # Type checking
89
+ poetry run mypy src/
90
+ ```
91
+
92
+ ## License
93
+
94
+ MIT
@@ -0,0 +1,69 @@
1
+ # mongo-orm
2
+
3
+ A MongoDB ORM for Python
4
+
5
+ ## Usage
6
+
7
+ ### Defining an Entity
8
+
9
+ Use the `@entity` decorator to map a class to a MongoDB collection. Your class should inherit from `BaseEntity`.
10
+
11
+ ```python
12
+ from mongo_orm import BaseEntity, entity
13
+
14
+ @entity("users")
15
+ class User(BaseEntity):
16
+ name: str
17
+ email: str
18
+
19
+ # Define indexes
20
+ __indexes__ = [
21
+ {"keys": [("email", 1)], "unique": True},
22
+ {"keys": [("name", 1)]}
23
+ ]
24
+ ```
25
+
26
+ ### Index Management
27
+
28
+ The ORM can automatically manage your MongoDB indexes based on the `__indexes__` attribute. This behavior is controlled by the `MONGODB_INDEX_AUTOAPPLY` environment variable.
29
+
30
+ #### Configuration (`MONGODB_INDEX_AUTOAPPLY`)
31
+
32
+ - `never` (default): No automatic index management.
33
+ - `always`: Checks and applies indexes every time an entity class is initialized (on startup).
34
+ - `auto-lock`: Checks and applies indexes once. After a successful check, it writes a hash of the index to `mongo-orm.lock`. Subsequent startups will skip the check if the hash is present in the lock file.
35
+
36
+ #### Manual Index Application
37
+
38
+ You can also trigger index application manually for all registered entities:
39
+
40
+ ```python
41
+ from mongo_orm.utils import apply_all_indexes
42
+
43
+ # Apply indexes using the current environment configuration
44
+ apply_all_indexes()
45
+
46
+ # Or force a specific mode
47
+ apply_all_indexes(mode="always")
48
+ ```
49
+
50
+ ## Development
51
+
52
+ ```bash
53
+ # Install dependencies
54
+ poetry install
55
+
56
+ # Run tests
57
+ poetry run pytest
58
+
59
+ # Format code
60
+ poetry run black .
61
+ poetry run isort .
62
+
63
+ # Type checking
64
+ poetry run mypy src/
65
+ ```
66
+
67
+ ## License
68
+
69
+ 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.1"
8
8
  description = "A MongoDB ORM for Python"
9
9
  readme = "README.md"
10
10
  authors = ["Juraj Bezdek <juraj.bezdek@gmail.com>"]
@@ -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,6 +54,7 @@ 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:
@@ -68,6 +78,33 @@ REPOSITORY = Repository()
68
78
 
69
79
 
70
80
  T = TypeVar("T", bound="BaseEntity")
81
+ Keys = Union[
82
+ str,
83
+ List[Union[str, Tuple[str, int]]],
84
+ Dict[str, int],
85
+ ]
86
+
87
+
88
+ class IndexSpec(TypedDict, total=False):
89
+ """
90
+ Typed dict describing a MongoDB index.
91
+
92
+ Examples:
93
+ {"keys": "email", "unique": True}
94
+ {"keys": [("tenant_id", 1), ("created_at", -1)], "name": "tenant_created_idx"}
95
+ {"keys": {"email": 1}, "unique": True}
96
+ """
97
+
98
+ keys: Keys
99
+ # Optional index options
100
+ unique: bool
101
+ sparse: bool
102
+ background: bool
103
+ name: str
104
+ expireAfterSeconds: int
105
+ expire_after_seconds: int # alias
106
+ partialFilterExpression: Dict[str, Any]
107
+ weights: Dict[str, int]
71
108
 
72
109
 
73
110
  class BaseEntity(BaseModel):
@@ -81,6 +118,8 @@ class BaseEntity(BaseModel):
81
118
  _loaded_at: datetime.datetime | None = PrivateAttr(None)
82
119
  _tracked_fields_state: dict | None = PrivateAttr(None)
83
120
 
121
+ __indexes__: Optional[List[IndexSpec]] = None
122
+
84
123
  def __init__(self, **data) -> None:
85
124
  if not hasattr(self.__class__, "collection"):
86
125
  raise Exception(
@@ -557,6 +596,13 @@ def entity(
557
596
 
558
597
  cls.version_field = version_field
559
598
 
599
+ if cls not in REPOSITORY.all_entities:
600
+ REPOSITORY.all_entities.append(cls)
601
+
602
+ from .utils import apply_indexes
603
+
604
+ apply_indexes(cls)
605
+
560
606
  return cls
561
607
 
562
608
  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