mongoforge-odm 0.1.0__py3-none-any.whl
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.
- mongoforge/__init__.py +5 -0
- mongoforge/client.py +19 -0
- mongoforge/exceptions.py +59 -0
- mongoforge/fields/__init__.py +24 -0
- mongoforge/fields/base.py +63 -0
- mongoforge/fields/types.py +149 -0
- mongoforge/fields/utils.py +0 -0
- mongoforge/managers/__init__.py +5 -0
- mongoforge/managers/base.py +16 -0
- mongoforge/managers/create.py +25 -0
- mongoforge/managers/delete.py +25 -0
- mongoforge/managers/helpers.py +20 -0
- mongoforge/managers/read.py +73 -0
- mongoforge/managers/update.py +56 -0
- mongoforge/meta.py +114 -0
- mongoforge/models.py +132 -0
- mongoforge/settings.py +7 -0
- mongoforge/utils/__init__.py +8 -0
- mongoforge/utils/collections.py +0 -0
- mongoforge/utils/dates.py +4 -0
- mongoforge/utils/validation.py +4 -0
- mongoforge_odm-0.1.0.dist-info/METADATA +232 -0
- mongoforge_odm-0.1.0.dist-info/RECORD +25 -0
- mongoforge_odm-0.1.0.dist-info/WHEEL +5 -0
- mongoforge_odm-0.1.0.dist-info/top_level.txt +1 -0
mongoforge/__init__.py
ADDED
mongoforge/client.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from pymongo import MongoClient
|
|
2
|
+
from pymongo.errors import PyMongoError
|
|
3
|
+
|
|
4
|
+
from .settings import MONGO_URI
|
|
5
|
+
from .exceptions import DatabaseConnectionError
|
|
6
|
+
|
|
7
|
+
_client = None
|
|
8
|
+
|
|
9
|
+
def get_client():
|
|
10
|
+
global _client
|
|
11
|
+
try:
|
|
12
|
+
if _client is None:
|
|
13
|
+
_client = MongoClient(MONGO_URI)
|
|
14
|
+
_client.admin.command("ping")
|
|
15
|
+
return _client
|
|
16
|
+
except PyMongoError as e:
|
|
17
|
+
raise DatabaseConnectionError(
|
|
18
|
+
f"Failed to connect to MongoDB: {e}"
|
|
19
|
+
) from e
|
mongoforge/exceptions.py
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Dict, List
|
|
4
|
+
|
|
5
|
+
class MongoForgeError(Exception):
|
|
6
|
+
"""Base exception for all MongoForge errors."""
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class DatabaseConnectionError(MongoForgeError):
|
|
10
|
+
"""Raised when MongoForge cannot connect to MongoDB."""
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class CollectionError(MongoForgeError):
|
|
14
|
+
"""Raised when a collection-related operation fails."""
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ValidationError(MongoForgeError):
|
|
18
|
+
"""
|
|
19
|
+
Raised when document validation fails.
|
|
20
|
+
|
|
21
|
+
Example:
|
|
22
|
+
raise ValidationError({
|
|
23
|
+
"email": ["This field is required"],
|
|
24
|
+
"age": ["Must be greater than 0"]
|
|
25
|
+
})
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def __init__(self, errors: Dict[str, List[str]]) -> None:
|
|
29
|
+
self.errors = errors
|
|
30
|
+
|
|
31
|
+
message = self._build_message(errors)
|
|
32
|
+
super().__init__(message)
|
|
33
|
+
|
|
34
|
+
@staticmethod
|
|
35
|
+
def _build_message(errors: Dict[str, List[str]]) -> str:
|
|
36
|
+
if not errors:
|
|
37
|
+
return "Validation failed."
|
|
38
|
+
|
|
39
|
+
parts = []
|
|
40
|
+
|
|
41
|
+
for field, field_errors in errors.items():
|
|
42
|
+
if not field_errors:
|
|
43
|
+
continue
|
|
44
|
+
|
|
45
|
+
parts.append(f"{field}: {', '.join(field_errors)}")
|
|
46
|
+
|
|
47
|
+
if not parts:
|
|
48
|
+
return "Validation failed."
|
|
49
|
+
|
|
50
|
+
return f"Validation failed. {'; '.join(parts)}"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class DocumentNotFoundError(MongoForgeError):
|
|
54
|
+
"""
|
|
55
|
+
Raised when a requested document cannot be found.
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
def __init__(self, message: str = "Document not found.") -> None:
|
|
59
|
+
super().__init__(message)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from .base import BaseField
|
|
2
|
+
from .types import (
|
|
3
|
+
StringField,
|
|
4
|
+
IntField,
|
|
5
|
+
FloatField,
|
|
6
|
+
BoolField,
|
|
7
|
+
DictField,
|
|
8
|
+
ObjectIdField,
|
|
9
|
+
ListField,
|
|
10
|
+
DateTimeField,
|
|
11
|
+
EmbeddedField,
|
|
12
|
+
)
|
|
13
|
+
__all__ = [
|
|
14
|
+
"BaseField",
|
|
15
|
+
"StringField",
|
|
16
|
+
"IntField",
|
|
17
|
+
"FloatField",
|
|
18
|
+
"BoolField",
|
|
19
|
+
"DictField",
|
|
20
|
+
"ObjectIdField",
|
|
21
|
+
"ListField",
|
|
22
|
+
"DateTimeField",
|
|
23
|
+
"EmbeddedField",
|
|
24
|
+
]
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
class BaseField:
|
|
4
|
+
python_type = object
|
|
5
|
+
|
|
6
|
+
def __init__(
|
|
7
|
+
self,
|
|
8
|
+
*,
|
|
9
|
+
required=False,
|
|
10
|
+
default=None,
|
|
11
|
+
unique=False,
|
|
12
|
+
choices=None,
|
|
13
|
+
validators=None,
|
|
14
|
+
null=False,
|
|
15
|
+
):
|
|
16
|
+
self.name = None
|
|
17
|
+
self.required = required
|
|
18
|
+
self.default = default
|
|
19
|
+
self.unique = unique
|
|
20
|
+
self.choices = choices or []
|
|
21
|
+
self.validators = validators or []
|
|
22
|
+
self.null = null
|
|
23
|
+
|
|
24
|
+
def get_default(self):
|
|
25
|
+
if callable(self.default):
|
|
26
|
+
return self.default()
|
|
27
|
+
return self.default
|
|
28
|
+
|
|
29
|
+
def to_python(self, value):
|
|
30
|
+
return value
|
|
31
|
+
|
|
32
|
+
def to_mongo(self, value):
|
|
33
|
+
return value
|
|
34
|
+
|
|
35
|
+
def validate(self, value):
|
|
36
|
+
errors = []
|
|
37
|
+
|
|
38
|
+
if value is None:
|
|
39
|
+
if self.required and not self.null:
|
|
40
|
+
errors.append("This field is required.")
|
|
41
|
+
return errors
|
|
42
|
+
|
|
43
|
+
if (
|
|
44
|
+
self.python_type is not object
|
|
45
|
+
and not isinstance(value, self.python_type)
|
|
46
|
+
):
|
|
47
|
+
errors.append(
|
|
48
|
+
f"Expected {self.python_type.__name__}, got {type(value).__name__}"
|
|
49
|
+
)
|
|
50
|
+
return errors
|
|
51
|
+
|
|
52
|
+
if self.choices and value not in self.choices:
|
|
53
|
+
errors.append(
|
|
54
|
+
f"Value must be one of: {self.choices}"
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
for validator in self.validators:
|
|
58
|
+
try:
|
|
59
|
+
validator(value)
|
|
60
|
+
except ValueError as e:
|
|
61
|
+
errors.append(str(e))
|
|
62
|
+
|
|
63
|
+
return errors
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
from .base import BaseField
|
|
2
|
+
import re
|
|
3
|
+
from bson import ObjectId
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
|
|
6
|
+
class StringField(BaseField):
|
|
7
|
+
python_type = str
|
|
8
|
+
def __init__(
|
|
9
|
+
self,
|
|
10
|
+
*,
|
|
11
|
+
min_length=None,
|
|
12
|
+
max_length=None,
|
|
13
|
+
regex=None,
|
|
14
|
+
**kwargs,
|
|
15
|
+
):
|
|
16
|
+
super().__init__(**kwargs)
|
|
17
|
+
self.min_length = min_length
|
|
18
|
+
self.max_length = max_length
|
|
19
|
+
self.regex = regex
|
|
20
|
+
|
|
21
|
+
def validate(self, value):
|
|
22
|
+
errors = super().validate(value)
|
|
23
|
+
if value is None:
|
|
24
|
+
return errors
|
|
25
|
+
|
|
26
|
+
if self.min_length is not None:
|
|
27
|
+
if len(value) < self.min_length:
|
|
28
|
+
errors.append(
|
|
29
|
+
f"Minimum length is {self.min_length}"
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
if self.max_length is not None:
|
|
33
|
+
if len(value) > self.max_length:
|
|
34
|
+
errors.append(
|
|
35
|
+
f"Maximum length is {self.max_length}"
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
if self.regex:
|
|
39
|
+
if re.match(self.regex, value) is None:
|
|
40
|
+
errors.append(
|
|
41
|
+
f"Value does not match regex {self.regex}"
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
return errors
|
|
45
|
+
|
|
46
|
+
class IntField(BaseField):
|
|
47
|
+
python_type = int
|
|
48
|
+
def __init__(
|
|
49
|
+
self,
|
|
50
|
+
*,
|
|
51
|
+
min_value=None,
|
|
52
|
+
max_value=None,
|
|
53
|
+
**kwargs,
|
|
54
|
+
):
|
|
55
|
+
super().__init__(**kwargs)
|
|
56
|
+
self.min_value = min_value
|
|
57
|
+
self.max_value = max_value
|
|
58
|
+
|
|
59
|
+
def validate(self, value):
|
|
60
|
+
errors = super().validate(value)
|
|
61
|
+
if value is None:
|
|
62
|
+
return errors
|
|
63
|
+
|
|
64
|
+
if self.min_value is not None:
|
|
65
|
+
if value < self.min_value:
|
|
66
|
+
errors.append(
|
|
67
|
+
f"Minimum value is {self.min_value}"
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
if self.max_value is not None:
|
|
71
|
+
if value > self.max_value:
|
|
72
|
+
errors.append(
|
|
73
|
+
f"Maximum value is {self.max_value}"
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
return errors
|
|
77
|
+
|
|
78
|
+
class FloatField(BaseField):
|
|
79
|
+
python_type = float
|
|
80
|
+
|
|
81
|
+
class BoolField(BaseField):
|
|
82
|
+
python_type = bool
|
|
83
|
+
|
|
84
|
+
class DictField(BaseField):
|
|
85
|
+
python_type = dict
|
|
86
|
+
|
|
87
|
+
class ObjectIdField(BaseField):
|
|
88
|
+
python_type = ObjectId
|
|
89
|
+
|
|
90
|
+
def to_mongo(self, value):
|
|
91
|
+
if value is None:
|
|
92
|
+
return ObjectId()
|
|
93
|
+
return value
|
|
94
|
+
|
|
95
|
+
class ListField(BaseField):
|
|
96
|
+
python_type = list
|
|
97
|
+
|
|
98
|
+
def __init__(self, field=None, **kwargs):
|
|
99
|
+
super().__init__(**kwargs)
|
|
100
|
+
self.field = field
|
|
101
|
+
|
|
102
|
+
def validate(self, value):
|
|
103
|
+
errors = super().validate(value)
|
|
104
|
+
|
|
105
|
+
if value is None:
|
|
106
|
+
return errors
|
|
107
|
+
|
|
108
|
+
if self.field:
|
|
109
|
+
for i, item in enumerate(value):
|
|
110
|
+
item_errors = self.field.validate(item)
|
|
111
|
+
for error in item_errors:
|
|
112
|
+
errors.append(f"[{i}] {error}")
|
|
113
|
+
|
|
114
|
+
return errors
|
|
115
|
+
|
|
116
|
+
class DateTimeField(BaseField):
|
|
117
|
+
python_type = datetime
|
|
118
|
+
|
|
119
|
+
def __init__(
|
|
120
|
+
self,
|
|
121
|
+
*,
|
|
122
|
+
auto_now=False,
|
|
123
|
+
auto_now_add=False,
|
|
124
|
+
**kwargs,
|
|
125
|
+
):
|
|
126
|
+
super().__init__(**kwargs)
|
|
127
|
+
self.auto_now = auto_now
|
|
128
|
+
self.auto_now_add = auto_now_add
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def validate(self, value):
|
|
132
|
+
if (
|
|
133
|
+
value is None
|
|
134
|
+
and (self.auto_now or self.auto_now_add)
|
|
135
|
+
):
|
|
136
|
+
return []
|
|
137
|
+
|
|
138
|
+
return super().validate(value)
|
|
139
|
+
|
|
140
|
+
class EmbeddedField(BaseField):
|
|
141
|
+
python_type = dict
|
|
142
|
+
|
|
143
|
+
def __init__(
|
|
144
|
+
self,
|
|
145
|
+
document_class=None,
|
|
146
|
+
**kwargs,
|
|
147
|
+
):
|
|
148
|
+
super().__init__(**kwargs)
|
|
149
|
+
self.document_class = document_class
|
|
File without changes
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from .helpers import ManagerHelper
|
|
2
|
+
from .create import CreateMixin
|
|
3
|
+
from .read import ReadMixin
|
|
4
|
+
from .update import UpdateMixin
|
|
5
|
+
from .delete import DeleteMixin
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class MongoManager(
|
|
9
|
+
ManagerHelper,
|
|
10
|
+
CreateMixin,
|
|
11
|
+
ReadMixin,
|
|
12
|
+
UpdateMixin,
|
|
13
|
+
DeleteMixin
|
|
14
|
+
):
|
|
15
|
+
def __init__(self, collection_name: str):
|
|
16
|
+
self._initialize(collection_name)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from pymongo.errors import PyMongoError
|
|
2
|
+
|
|
3
|
+
from ..exceptions import CollectionError
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class CreateMixin:
|
|
7
|
+
def create(self, data: dict):
|
|
8
|
+
try:
|
|
9
|
+
result = self._collection.insert_one(data)
|
|
10
|
+
return result.inserted_id
|
|
11
|
+
except PyMongoError as e:
|
|
12
|
+
raise CollectionError(
|
|
13
|
+
f"Failed to create document in "
|
|
14
|
+
f"'{self._collection_name}'"
|
|
15
|
+
) from e
|
|
16
|
+
|
|
17
|
+
def bulk_create(self, data: list[dict]):
|
|
18
|
+
try:
|
|
19
|
+
result = self._collection.insert_many(data)
|
|
20
|
+
return result.inserted_ids
|
|
21
|
+
except PyMongoError as e:
|
|
22
|
+
raise CollectionError(
|
|
23
|
+
f"Failed to bulk create documents in "
|
|
24
|
+
f"'{self._collection_name}'"
|
|
25
|
+
) from e
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from pymongo.errors import PyMongoError
|
|
2
|
+
|
|
3
|
+
from ..exceptions import CollectionError
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class DeleteMixin:
|
|
7
|
+
def delete_one(self, query: dict):
|
|
8
|
+
try:
|
|
9
|
+
result = self._collection.delete_one(query)
|
|
10
|
+
return result.deleted_count
|
|
11
|
+
except PyMongoError as e:
|
|
12
|
+
raise CollectionError(
|
|
13
|
+
f"Failed to delete document from "
|
|
14
|
+
f"'{self._collection_name}'"
|
|
15
|
+
) from e
|
|
16
|
+
|
|
17
|
+
def delete_many(self, query: dict):
|
|
18
|
+
try:
|
|
19
|
+
result = self._collection.delete_many(query)
|
|
20
|
+
return result.deleted_count
|
|
21
|
+
except PyMongoError as e:
|
|
22
|
+
raise CollectionError(
|
|
23
|
+
f"Failed to delete documents from "
|
|
24
|
+
f"'{self._collection_name}'"
|
|
25
|
+
) from e
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
|
|
2
|
+
from slugify import slugify
|
|
3
|
+
|
|
4
|
+
from ..client import get_client
|
|
5
|
+
from ..settings import DATABASE_NAME
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ManagerHelper:
|
|
9
|
+
def _initialize(self, collection_name: str):
|
|
10
|
+
self._client = get_client()
|
|
11
|
+
self._database = self._client[DATABASE_NAME]
|
|
12
|
+
|
|
13
|
+
self._collection_name = slugify(
|
|
14
|
+
collection_name,
|
|
15
|
+
separator="_"
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
self._collection = self._database[
|
|
19
|
+
self._collection_name
|
|
20
|
+
]
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
from typing import List
|
|
2
|
+
from pymongo.errors import PyMongoError
|
|
3
|
+
|
|
4
|
+
from ..exceptions import CollectionError
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class ReadMixin:
|
|
8
|
+
|
|
9
|
+
OPERATORS = {
|
|
10
|
+
"gt": "$gt",
|
|
11
|
+
"gte": "$gte",
|
|
12
|
+
"lt": "$lt",
|
|
13
|
+
"lte": "$lte",
|
|
14
|
+
"ne": "$ne",
|
|
15
|
+
"in": "$in",
|
|
16
|
+
"nin": "$nin",
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
def all(self) -> List[dict]:
|
|
20
|
+
try:
|
|
21
|
+
return list(self._collection.find())
|
|
22
|
+
except PyMongoError as e:
|
|
23
|
+
raise CollectionError(
|
|
24
|
+
f"Failed to fetch documents from "
|
|
25
|
+
f"'{self._collection_name}'"
|
|
26
|
+
) from e
|
|
27
|
+
|
|
28
|
+
def first(self):
|
|
29
|
+
try:
|
|
30
|
+
return self._collection.find_one()
|
|
31
|
+
except PyMongoError as e:
|
|
32
|
+
raise CollectionError(
|
|
33
|
+
f"Failed to fetch document from "
|
|
34
|
+
f"'{self._collection_name}'"
|
|
35
|
+
) from e
|
|
36
|
+
|
|
37
|
+
def get(self, query: dict):
|
|
38
|
+
try:
|
|
39
|
+
return self._collection.find_one(query)
|
|
40
|
+
except PyMongoError as e:
|
|
41
|
+
raise CollectionError(
|
|
42
|
+
f"Failed to fetch document from "
|
|
43
|
+
f"'{self._collection_name}'"
|
|
44
|
+
) from e
|
|
45
|
+
|
|
46
|
+
def filter(self, **kwargs):
|
|
47
|
+
query = {}
|
|
48
|
+
|
|
49
|
+
for key, value in kwargs.items():
|
|
50
|
+
if "__" in key:
|
|
51
|
+
field, op = key.split("__", 1)
|
|
52
|
+
mongo_op = self.OPERATORS.get(op)
|
|
53
|
+
|
|
54
|
+
if not mongo_op:
|
|
55
|
+
raise ValueError(
|
|
56
|
+
f"Unsupported operator: {op}"
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
query[field] = {mongo_op: value}
|
|
60
|
+
else:
|
|
61
|
+
query[key] = value
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
return list(self._collection.find(query))
|
|
65
|
+
|
|
66
|
+
def count(self):
|
|
67
|
+
try:
|
|
68
|
+
return self._collection.count_documents({})
|
|
69
|
+
except PyMongoError as e:
|
|
70
|
+
raise CollectionError(
|
|
71
|
+
f"Failed to count documents in "
|
|
72
|
+
f"'{self._collection_name}'"
|
|
73
|
+
) from e
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
from pymongo.errors import PyMongoError
|
|
2
|
+
from bson.objectid import ObjectId
|
|
3
|
+
from ..exceptions import CollectionError
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class UpdateMixin:
|
|
7
|
+
def update(
|
|
8
|
+
self,
|
|
9
|
+
id:int,
|
|
10
|
+
update_data:dict
|
|
11
|
+
):
|
|
12
|
+
try:
|
|
13
|
+
result = self._collection.update_one(
|
|
14
|
+
{"_id":ObjectId(id)},
|
|
15
|
+
{"$set": update_data}
|
|
16
|
+
)
|
|
17
|
+
return result.modified_count
|
|
18
|
+
except PyMongoError as e:
|
|
19
|
+
raise CollectionError(
|
|
20
|
+
f"Failed to update document in "
|
|
21
|
+
f"'{self._collection_name}'"
|
|
22
|
+
) from e
|
|
23
|
+
|
|
24
|
+
def update_one(
|
|
25
|
+
self,
|
|
26
|
+
filter_query: dict,
|
|
27
|
+
update_data: dict
|
|
28
|
+
):
|
|
29
|
+
try:
|
|
30
|
+
result = self._collection.update_one(
|
|
31
|
+
filter_query,
|
|
32
|
+
{"$set": update_data}
|
|
33
|
+
)
|
|
34
|
+
return result.modified_count
|
|
35
|
+
except PyMongoError as e:
|
|
36
|
+
raise CollectionError(
|
|
37
|
+
f"Failed to update document in "
|
|
38
|
+
f"'{self._collection_name}'"
|
|
39
|
+
) from e
|
|
40
|
+
|
|
41
|
+
def update_many(
|
|
42
|
+
self,
|
|
43
|
+
filter_query: dict,
|
|
44
|
+
update_data: dict
|
|
45
|
+
):
|
|
46
|
+
try:
|
|
47
|
+
result = self._collection.update_many(
|
|
48
|
+
filter_query,
|
|
49
|
+
{"$set": update_data}
|
|
50
|
+
)
|
|
51
|
+
return result.modified_count
|
|
52
|
+
except PyMongoError as e:
|
|
53
|
+
raise CollectionError(
|
|
54
|
+
f"Failed to update documents in "
|
|
55
|
+
f"'{self._collection_name}'"
|
|
56
|
+
) from e
|
mongoforge/meta.py
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
from slugify import slugify
|
|
2
|
+
|
|
3
|
+
from .fields.base import BaseField
|
|
4
|
+
from .managers import MongoManager
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Options:
|
|
8
|
+
def __init__(self):
|
|
9
|
+
self.collection = None
|
|
10
|
+
self.indexes = []
|
|
11
|
+
self.ordering = []
|
|
12
|
+
self.abstract = False
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class MongoForgeMeta(type):
|
|
16
|
+
"""
|
|
17
|
+
Metaclass responsible for:
|
|
18
|
+
|
|
19
|
+
- Collecting fields
|
|
20
|
+
- Handling inheritance
|
|
21
|
+
- Building model metadata
|
|
22
|
+
- Attaching MongoManager
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __new__(mcls, name, bases, attrs):
|
|
26
|
+
# -------------------------
|
|
27
|
+
# 1. Inherit parent fields
|
|
28
|
+
# -------------------------
|
|
29
|
+
fields = {}
|
|
30
|
+
|
|
31
|
+
for base in bases:
|
|
32
|
+
if hasattr(base, "_fields"):
|
|
33
|
+
fields.update(base._fields)
|
|
34
|
+
|
|
35
|
+
# -------------------------
|
|
36
|
+
# 2. Collect declared fields
|
|
37
|
+
# -------------------------
|
|
38
|
+
for attr_name, value in list(attrs.items()):
|
|
39
|
+
if isinstance(value, BaseField):
|
|
40
|
+
value.name = attr_name
|
|
41
|
+
fields[attr_name] = value
|
|
42
|
+
|
|
43
|
+
# Prevent class attribute shadowing
|
|
44
|
+
attrs.pop(attr_name)
|
|
45
|
+
|
|
46
|
+
# -------------------------
|
|
47
|
+
# 3. Create class
|
|
48
|
+
# -------------------------
|
|
49
|
+
cls = super().__new__(mcls, name, bases, attrs)
|
|
50
|
+
|
|
51
|
+
cls._fields = fields
|
|
52
|
+
|
|
53
|
+
# -------------------------
|
|
54
|
+
# 4. Build Meta options
|
|
55
|
+
# -------------------------
|
|
56
|
+
options = Options()
|
|
57
|
+
|
|
58
|
+
# inherit parent meta
|
|
59
|
+
for base in bases:
|
|
60
|
+
if hasattr(base, "_meta"):
|
|
61
|
+
parent = base._meta
|
|
62
|
+
|
|
63
|
+
options.collection = parent.collection
|
|
64
|
+
options.indexes = list(parent.indexes)
|
|
65
|
+
options.ordering = list(parent.ordering)
|
|
66
|
+
options.abstract = parent.abstract
|
|
67
|
+
|
|
68
|
+
break
|
|
69
|
+
|
|
70
|
+
meta = attrs.get("Meta")
|
|
71
|
+
|
|
72
|
+
if meta:
|
|
73
|
+
options.collection = getattr(
|
|
74
|
+
meta,
|
|
75
|
+
"collection",
|
|
76
|
+
options.collection,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
options.indexes = getattr(
|
|
80
|
+
meta,
|
|
81
|
+
"indexes",
|
|
82
|
+
options.indexes,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
options.ordering = getattr(
|
|
86
|
+
meta,
|
|
87
|
+
"ordering",
|
|
88
|
+
options.ordering,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
options.abstract = getattr(
|
|
92
|
+
meta,
|
|
93
|
+
"abstract",
|
|
94
|
+
options.abstract,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
# -------------------------
|
|
98
|
+
# 5. Default collection name
|
|
99
|
+
# -------------------------
|
|
100
|
+
if not options.collection:
|
|
101
|
+
options.collection = slugify(name)
|
|
102
|
+
|
|
103
|
+
cls._meta = options
|
|
104
|
+
|
|
105
|
+
# -------------------------
|
|
106
|
+
# 6. Attach manager
|
|
107
|
+
# -------------------------
|
|
108
|
+
if (
|
|
109
|
+
name != "MongoForge"
|
|
110
|
+
and not options.abstract
|
|
111
|
+
):
|
|
112
|
+
cls.objects = MongoManager(options.collection)
|
|
113
|
+
|
|
114
|
+
return cls
|
mongoforge/models.py
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
|
|
3
|
+
from .exceptions import ValidationError
|
|
4
|
+
from .fields.types import DateTimeField
|
|
5
|
+
from .meta import MongoForgeMeta
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class MongoForge(metaclass=MongoForgeMeta):
|
|
9
|
+
"""
|
|
10
|
+
Base class for all MongoForge models.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
def __init__(self, **kwargs):
|
|
14
|
+
# Internal state
|
|
15
|
+
super().__setattr__("_data", {})
|
|
16
|
+
super().__setattr__("_is_new", True)
|
|
17
|
+
|
|
18
|
+
# Populate declared fields
|
|
19
|
+
for field_name, field in self._fields.items():
|
|
20
|
+
if field_name in kwargs:
|
|
21
|
+
value = kwargs.pop(field_name)
|
|
22
|
+
else:
|
|
23
|
+
value = field.get_default()
|
|
24
|
+
|
|
25
|
+
self._data[field_name] = value
|
|
26
|
+
|
|
27
|
+
# Store unknown fields (strict=False)
|
|
28
|
+
for key, value in kwargs.items():
|
|
29
|
+
self._data[key] = value
|
|
30
|
+
|
|
31
|
+
def __getattr__(self, name):
|
|
32
|
+
"""
|
|
33
|
+
Return declared field values from _data.
|
|
34
|
+
"""
|
|
35
|
+
if name in self._fields:
|
|
36
|
+
return self._data.get(name)
|
|
37
|
+
|
|
38
|
+
raise AttributeError(
|
|
39
|
+
f"{self.__class__.__name__!r} object has no attribute {name!r}"
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
def __setattr__(self, name, value):
|
|
43
|
+
"""
|
|
44
|
+
Store declared fields inside _data.
|
|
45
|
+
"""
|
|
46
|
+
# Internal/private attributes
|
|
47
|
+
if name.startswith("_"):
|
|
48
|
+
super().__setattr__(name, value)
|
|
49
|
+
return
|
|
50
|
+
|
|
51
|
+
# Model fields
|
|
52
|
+
if hasattr(self, "_fields") and name in self._fields:
|
|
53
|
+
self._data[name] = value
|
|
54
|
+
return
|
|
55
|
+
|
|
56
|
+
# Normal Python attribute
|
|
57
|
+
super().__setattr__(name, value)
|
|
58
|
+
|
|
59
|
+
def __repr__(self):
|
|
60
|
+
values = ", ".join(
|
|
61
|
+
f"{field}={self._data.get(field)!r}"
|
|
62
|
+
for field in self._fields
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
return f"<{self.__class__.__name__} {values}>"
|
|
66
|
+
|
|
67
|
+
def validate(self):
|
|
68
|
+
"""
|
|
69
|
+
Validate every declared field.
|
|
70
|
+
"""
|
|
71
|
+
errors = {}
|
|
72
|
+
|
|
73
|
+
for field_name, field in self._fields.items():
|
|
74
|
+
field_errors = field.validate(
|
|
75
|
+
self._data.get(field_name)
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
if field_errors:
|
|
79
|
+
errors[field_name] = field_errors
|
|
80
|
+
|
|
81
|
+
if errors:
|
|
82
|
+
raise ValidationError(errors)
|
|
83
|
+
|
|
84
|
+
def to_mongo(self):
|
|
85
|
+
"""
|
|
86
|
+
Serialize model into a MongoDB document.
|
|
87
|
+
"""
|
|
88
|
+
document = {}
|
|
89
|
+
|
|
90
|
+
for field_name, field in self._fields.items():
|
|
91
|
+
value = self._data.get(field_name)
|
|
92
|
+
|
|
93
|
+
# Handle DateTime auto fields
|
|
94
|
+
if isinstance(field, DateTimeField):
|
|
95
|
+
|
|
96
|
+
if field.auto_now:
|
|
97
|
+
value = datetime.utcnow()
|
|
98
|
+
self._data[field_name] = value
|
|
99
|
+
|
|
100
|
+
elif field.auto_now_add and value is None:
|
|
101
|
+
value = datetime.utcnow()
|
|
102
|
+
self._data[field_name] = value
|
|
103
|
+
|
|
104
|
+
document[field_name] = field.to_mongo(value)
|
|
105
|
+
|
|
106
|
+
# Preserve MongoDB ObjectId
|
|
107
|
+
if "_id" in self._data:
|
|
108
|
+
document["_id"] = self._data["_id"]
|
|
109
|
+
|
|
110
|
+
return document
|
|
111
|
+
|
|
112
|
+
@classmethod
|
|
113
|
+
def from_mongo(cls, raw_doc):
|
|
114
|
+
"""
|
|
115
|
+
Create a model instance from a MongoDB document.
|
|
116
|
+
"""
|
|
117
|
+
instance = cls()
|
|
118
|
+
|
|
119
|
+
instance._data = {}
|
|
120
|
+
|
|
121
|
+
for field_name, field in cls._fields.items():
|
|
122
|
+
raw_value = raw_doc.get(field_name)
|
|
123
|
+
|
|
124
|
+
instance._data[field_name] = field.to_python(raw_value)
|
|
125
|
+
|
|
126
|
+
# Preserve _id
|
|
127
|
+
if "_id" in raw_doc:
|
|
128
|
+
instance._data["_id"] = raw_doc["_id"]
|
|
129
|
+
|
|
130
|
+
instance._is_new = False
|
|
131
|
+
|
|
132
|
+
return instance
|
mongoforge/settings.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mongoforge-odm
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A Django-inspired MongoDB ODM for Python
|
|
5
|
+
Author: Saurabh Singh
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/Sourabh7singh/MongoForge
|
|
8
|
+
Project-URL: Repository, https://github.com/Sourabh7singh/MongoForge
|
|
9
|
+
Project-URL: Issues, https://github.com/Sourabh7singh/MongoForge/issues
|
|
10
|
+
Keywords: mongodb,odm,orm,pymongo,django,schema,validation
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
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: Topic :: Database
|
|
19
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
Requires-Dist: pymongo>=4.6
|
|
23
|
+
Requires-Dist: python-dotenv>=1.0
|
|
24
|
+
Requires-Dist: python-slugify>=8.0
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
27
|
+
Requires-Dist: mongomock>=4.0; extra == "dev"
|
|
28
|
+
|
|
29
|
+
# π§ MongoForge
|
|
30
|
+
|
|
31
|
+
A **Django-inspired MongoDB ODM** for Python β define document schemas as classes with typed fields, validate data automatically, and perform CRUD operations through a clean manager API.
|
|
32
|
+
|
|
33
|
+
Built on [PyMongo](https://pymongo.readthedocs.io/).
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## β¨ Features
|
|
38
|
+
|
|
39
|
+
- **Declarative schemas** β define documents as Python classes with typed fields
|
|
40
|
+
- **10 built-in field types** β `StringField`, `IntField`, `FloatField`, `BoolField`, `ListField`, `DateTimeField`, `DictField`, `ObjectIdField`, `EmbeddedField` + extensible `BaseField`
|
|
41
|
+
- **Rich validation** β required, min/max length, min/max value, regex, choices, custom validators
|
|
42
|
+
- **Metaclass magic** β automatic field collection, inheritance, collection naming
|
|
43
|
+
- **Manager API** β `create`, `bulk_create`, `all`, `first`, `get`, `filter`, `count`, `update`, `update_one`, `update_many`, `delete_one`, `delete_many`
|
|
44
|
+
- **Django-style filter operators** β `age__gte=18`, `name__in=[...]`, `status__ne="inactive"` (supports: `gt`, `gte`, `lt`, `lte`, `ne`, `in`, `nin`)
|
|
45
|
+
- **Auto timestamps** β `DateTimeField(auto_now=True)` and `auto_now_add=True`
|
|
46
|
+
- **Attribute access** β `user.name` instead of `user["name"]`
|
|
47
|
+
- **Serialization** β `to_mongo()` and `from_mongo()` for round-trip conversion
|
|
48
|
+
- **Abstract models & inheritance** β share fields across model hierarchies
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## π¦ Installation
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
# Clone the repository
|
|
56
|
+
git clone https://github.com/yourusername/MongoForge.git
|
|
57
|
+
cd MongoForge
|
|
58
|
+
|
|
59
|
+
# Create and activate a virtual environment
|
|
60
|
+
python -m venv env
|
|
61
|
+
env\Scripts\activate # Windows
|
|
62
|
+
# source env/bin/activate # macOS/Linux
|
|
63
|
+
|
|
64
|
+
# Install dependencies
|
|
65
|
+
pip install pymongo python-dotenv python-slugify
|
|
66
|
+
|
|
67
|
+
# For running tests (no live MongoDB needed)
|
|
68
|
+
pip install pytest mongomock
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Environment Setup
|
|
72
|
+
|
|
73
|
+
Create a `.env` file in the project root:
|
|
74
|
+
|
|
75
|
+
```env
|
|
76
|
+
MONGO_URI=mongodb://localhost:27017/
|
|
77
|
+
DATABASE_NAME=mongoforge
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## π Quick Start
|
|
83
|
+
|
|
84
|
+
### Define a Model
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
from mongoforge import MongoForge
|
|
88
|
+
from mongoforge.fields import StringField, IntField, BoolField, DateTimeField, ListField
|
|
89
|
+
|
|
90
|
+
class User(MongoForge):
|
|
91
|
+
name = StringField(required=True, min_length=2, max_length=100)
|
|
92
|
+
email = StringField(required=True, regex=r"^[\w.+-]+@[\w-]+\.[\w.-]+$")
|
|
93
|
+
age = IntField(min_value=0, max_value=150)
|
|
94
|
+
is_admin = BoolField(default=False)
|
|
95
|
+
tags = ListField(StringField())
|
|
96
|
+
created = DateTimeField(auto_now_add=True)
|
|
97
|
+
updated = DateTimeField(auto_now=True)
|
|
98
|
+
|
|
99
|
+
class Meta:
|
|
100
|
+
collection = "users"
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Create & Validate
|
|
104
|
+
|
|
105
|
+
```python
|
|
106
|
+
user = User(name="Saurabh", email="saurabh@example.com", age=25)
|
|
107
|
+
|
|
108
|
+
# Validate before saving
|
|
109
|
+
user.validate() # raises ValidationError if invalid
|
|
110
|
+
|
|
111
|
+
# Serialize to MongoDB document
|
|
112
|
+
doc = user.to_mongo()
|
|
113
|
+
|
|
114
|
+
# Insert into database
|
|
115
|
+
User.objects.create(doc)
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### Query
|
|
119
|
+
|
|
120
|
+
```python
|
|
121
|
+
# Get all documents
|
|
122
|
+
User.objects.all() # β list[dict]
|
|
123
|
+
|
|
124
|
+
# Filter with operators
|
|
125
|
+
User.objects.filter(age__gte=18) # age >= 18
|
|
126
|
+
User.objects.filter(name__in=["Alice", "Bob"])
|
|
127
|
+
|
|
128
|
+
# Get single document
|
|
129
|
+
User.objects.get({"email": "saurabh@example.com"})
|
|
130
|
+
|
|
131
|
+
# Count
|
|
132
|
+
User.objects.count()
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### Update & Delete
|
|
136
|
+
|
|
137
|
+
```python
|
|
138
|
+
# Update by ID
|
|
139
|
+
User.objects.update(id="...", update_data={"name": "Updated Name"})
|
|
140
|
+
|
|
141
|
+
# Update many
|
|
142
|
+
User.objects.update_many({"age": {"$gte": 30}}, {"is_admin": True})
|
|
143
|
+
|
|
144
|
+
# Delete
|
|
145
|
+
User.objects.delete_one({"name": "Alice"})
|
|
146
|
+
User.objects.delete_many({"age": {"$lt": 18}})
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### Validation in Action
|
|
150
|
+
|
|
151
|
+
```python
|
|
152
|
+
from mongoforge.exceptions import ValidationError
|
|
153
|
+
|
|
154
|
+
user = User(name="A", email="not-an-email", age=-5)
|
|
155
|
+
try:
|
|
156
|
+
user.validate()
|
|
157
|
+
except ValidationError as exc:
|
|
158
|
+
print(exc)
|
|
159
|
+
# Validation failed. name: Minimum length is 2; email: Value does not match regex ...; age: Minimum value is 0
|
|
160
|
+
print(exc.errors)
|
|
161
|
+
# {'name': ['Minimum length is 2'], 'email': ['Value does not match regex ...'], 'age': ['Minimum value is 0']}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
## π Field Types
|
|
167
|
+
|
|
168
|
+
| Field | Python Type | Extra Options |
|
|
169
|
+
|---|---|---|
|
|
170
|
+
| `StringField` | `str` | `min_length`, `max_length`, `regex` |
|
|
171
|
+
| `IntField` | `int` | `min_value`, `max_value` |
|
|
172
|
+
| `FloatField` | `float` | β |
|
|
173
|
+
| `BoolField` | `bool` | β |
|
|
174
|
+
| `ListField` | `list` | `field` (inner field for item validation) |
|
|
175
|
+
| `DateTimeField` | `datetime` | `auto_now`, `auto_now_add` |
|
|
176
|
+
| `DictField` | `dict` | β |
|
|
177
|
+
| `ObjectIdField` | `ObjectId` | Auto-generates if `None` |
|
|
178
|
+
| `EmbeddedField` | `dict` | `document_class` (stub β Phase 4) |
|
|
179
|
+
|
|
180
|
+
**Common options** (all fields): `required`, `default`, `unique`, `choices`, `validators`, `null`
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
## ποΈ Architecture
|
|
185
|
+
|
|
186
|
+
```
|
|
187
|
+
Model Class β MongoForgeMeta (metaclass) β collects fields + attaches MongoManager
|
|
188
|
+
β
|
|
189
|
+
βββββββββββββββββββββββββββββββββββββββ
|
|
190
|
+
βΌ
|
|
191
|
+
MongoManager (cls.objects)
|
|
192
|
+
βββ CreateMixin β create(), bulk_create()
|
|
193
|
+
βββ ReadMixin β all(), first(), get(), filter(), count()
|
|
194
|
+
βββ UpdateMixin β update(), update_one(), update_many()
|
|
195
|
+
βββ DeleteMixin β delete_one(), delete_many()
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
---
|
|
199
|
+
|
|
200
|
+
## π§ͺ Running Tests
|
|
201
|
+
|
|
202
|
+
Tests use [mongomock](https://github.com/mongomock/mongomock) β **no live MongoDB required**.
|
|
203
|
+
|
|
204
|
+
```bash
|
|
205
|
+
# With venv activated
|
|
206
|
+
python -m pytest mongoforge/tests/ -v
|
|
207
|
+
|
|
208
|
+
# Or using venv python directly
|
|
209
|
+
env\Scripts\python.exe -m pytest mongoforge/tests/ -v
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
**148 tests** covering:
|
|
213
|
+
- All 10 field types + validation paths
|
|
214
|
+
- Model instances, attribute access, serialization
|
|
215
|
+
- Metaclass, inheritance, abstract models
|
|
216
|
+
- Full CRUD operations + filter operators
|
|
217
|
+
- Exception hierarchy + formatting
|
|
218
|
+
|
|
219
|
+
---
|
|
220
|
+
|
|
221
|
+
## πΊοΈ Roadmap
|
|
222
|
+
|
|
223
|
+
| Phase | Description | Status |
|
|
224
|
+
|---|---|---|
|
|
225
|
+
| **Phase 1** | Fields & Schema | β
~95% |
|
|
226
|
+
| **Phase 2** | QuerySet (chainable queries) | β¬ Not started |
|
|
227
|
+
| **Phase 3** | Model Lifecycle (`save()`, `delete()`, `reload()`, dirty tracking) | πΆ ~50% |
|
|
228
|
+
| **Phase 4** | Relationships (EmbeddedDocument, ReferenceField) | β¬ Not started |
|
|
229
|
+
| **Phase 5** | Indexes, Middleware & CLI | β¬ Not started |
|
|
230
|
+
| **Phase 6** | Packaging & Developer Experience | πΆ ~30% |
|
|
231
|
+
|
|
232
|
+
See [implementation_plan.md](implementation_plan.md) for the full roadmap with target APIs.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
mongoforge/__init__.py,sha256=0qHH5iKrjQvEFacaXUDwYAyg1S91WBedPP17oTJx_Z0,66
|
|
2
|
+
mongoforge/client.py,sha256=WSpyvQOdjBLG6FouAByhPvFijp8LF0b_E_8qK6Qewys,506
|
|
3
|
+
mongoforge/exceptions.py,sha256=5n6e6xqqTX4bOhMQtHbRQKaQjx1-lZfC9P_oP9FUCd8,1529
|
|
4
|
+
mongoforge/meta.py,sha256=DZaCan-L76W_YIU19bxiGr5aMp8dVCgSO-mnb9FI1RY,3015
|
|
5
|
+
mongoforge/models.py,sha256=oFGbw0ZN-FmCcBhRvTWfRt6bt4pImSThZH6Jo0434Hk,3628
|
|
6
|
+
mongoforge/settings.py,sha256=x1WXE4TRh69Ez0q4bo9kHOo1HUOHkQH3J_OKSojLsJ8,140
|
|
7
|
+
mongoforge/fields/__init__.py,sha256=vWLGCTLysk1DyOV0KtR6DnQlZOGVZd1H6WP_8DMFl7M,420
|
|
8
|
+
mongoforge/fields/base.py,sha256=HXvbIGscKyjk03k0wTe_0yUV8K0yvxBtOPP3t8C21ks,1594
|
|
9
|
+
mongoforge/fields/types.py,sha256=nnucR4nzr8Xsk9YdJAxlxICcbQ286ns1Ot5L7_uCl5o,3784
|
|
10
|
+
mongoforge/fields/utils.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
11
|
+
mongoforge/managers/__init__.py,sha256=fQVnl-ccLPoNMXZma_FyGhfJZ43N-qOaFr4lzr3b9S0,68
|
|
12
|
+
mongoforge/managers/base.py,sha256=jm7UyuEqkiwbHWZaCy2goMLIPLY4kFJlo8UiliIpUTY,370
|
|
13
|
+
mongoforge/managers/create.py,sha256=R6mMF4ZIRzR31AH-vG7zrAfTHJkZga2rBX0raEisEOE,794
|
|
14
|
+
mongoforge/managers/delete.py,sha256=e1zQCbBdiwkPFMnE8nIkTj3dTezUTC4PzNusvqbFGy4,798
|
|
15
|
+
mongoforge/managers/helpers.py,sha256=w590l0pwxlmk9tzFe34gF2jIhT9NjVie76LoJtGgCRU,476
|
|
16
|
+
mongoforge/managers/read.py,sha256=1bqAalJBnT9KFrjQE_JOmPyuncW7vzzE4kvt6PLdMQA,2008
|
|
17
|
+
mongoforge/managers/update.py,sha256=2xKpXOqRC9aZj_F3JEExaHT_0K6E09gdA0GnyX-wdQQ,1586
|
|
18
|
+
mongoforge/utils/__init__.py,sha256=2zJezdz2lUsVW_6XlRD4pHwGHtSl910pzQ3SNENFSnI,123
|
|
19
|
+
mongoforge/utils/collections.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
20
|
+
mongoforge/utils/dates.py,sha256=Au689mG57fkOVpyrw9WnqsPYyiID2Pqaa8SXfov10FA,76
|
|
21
|
+
mongoforge/utils/validation.py,sha256=xHTtaSCXfgccrQGjMUqbHvjsr1kP6ITdC0nSWKV6WpE,97
|
|
22
|
+
mongoforge_odm-0.1.0.dist-info/METADATA,sha256=vo_mxVDEfUqmQtEIMlIjFfM33JpRKIerv2o0ZcUPBQg,7756
|
|
23
|
+
mongoforge_odm-0.1.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
|
|
24
|
+
mongoforge_odm-0.1.0.dist-info/top_level.txt,sha256=n8InB-9S-tZoTZEPR7n0-BPP5hHGULkAP7Xfq-uV8jE,11
|
|
25
|
+
mongoforge_odm-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
mongoforge
|