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 ADDED
@@ -0,0 +1,5 @@
1
+ from .models import MongoForge
2
+
3
+ __all__ = [
4
+ "MongoForge"
5
+ ]
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
@@ -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,5 @@
1
+ from .base import MongoManager
2
+
3
+ __all__ = [
4
+ "MongoManager"
5
+ ]
@@ -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
@@ -0,0 +1,7 @@
1
+ from dotenv import load_dotenv
2
+ import os
3
+
4
+ load_dotenv()
5
+
6
+ MONGO_URI = os.getenv("MONGO_URI")
7
+ DATABASE_NAME = os.getenv("DATABASE_NAME")
@@ -0,0 +1,8 @@
1
+ from .dates import utcnow
2
+ from .validation import validate_regex
3
+
4
+
5
+ __all__ = [
6
+ "utcnow",
7
+ "validate_regex",
8
+ ]
File without changes
@@ -0,0 +1,4 @@
1
+ from datetime import datetime
2
+
3
+ def utcnow():
4
+ return datetime.utcnow()
@@ -0,0 +1,4 @@
1
+ import re
2
+
3
+ def validate_regex(value, pattern):
4
+ return re.match(pattern, value) is not None
@@ -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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (83.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ mongoforge