mongoforge-odm 0.1.0__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.
- mongoforge_odm-0.1.0/PKG-INFO +232 -0
- mongoforge_odm-0.1.0/README.md +204 -0
- mongoforge_odm-0.1.0/mongoforge/__init__.py +5 -0
- mongoforge_odm-0.1.0/mongoforge/client.py +19 -0
- mongoforge_odm-0.1.0/mongoforge/exceptions.py +59 -0
- mongoforge_odm-0.1.0/mongoforge/fields/__init__.py +24 -0
- mongoforge_odm-0.1.0/mongoforge/fields/base.py +63 -0
- mongoforge_odm-0.1.0/mongoforge/fields/types.py +149 -0
- mongoforge_odm-0.1.0/mongoforge/fields/utils.py +0 -0
- mongoforge_odm-0.1.0/mongoforge/managers/__init__.py +5 -0
- mongoforge_odm-0.1.0/mongoforge/managers/base.py +16 -0
- mongoforge_odm-0.1.0/mongoforge/managers/create.py +25 -0
- mongoforge_odm-0.1.0/mongoforge/managers/delete.py +25 -0
- mongoforge_odm-0.1.0/mongoforge/managers/helpers.py +20 -0
- mongoforge_odm-0.1.0/mongoforge/managers/read.py +73 -0
- mongoforge_odm-0.1.0/mongoforge/managers/update.py +56 -0
- mongoforge_odm-0.1.0/mongoforge/meta.py +114 -0
- mongoforge_odm-0.1.0/mongoforge/models.py +132 -0
- mongoforge_odm-0.1.0/mongoforge/settings.py +7 -0
- mongoforge_odm-0.1.0/mongoforge/utils/__init__.py +8 -0
- mongoforge_odm-0.1.0/mongoforge/utils/collections.py +0 -0
- mongoforge_odm-0.1.0/mongoforge/utils/dates.py +4 -0
- mongoforge_odm-0.1.0/mongoforge/utils/validation.py +4 -0
- mongoforge_odm-0.1.0/mongoforge_odm.egg-info/PKG-INFO +232 -0
- mongoforge_odm-0.1.0/mongoforge_odm.egg-info/SOURCES.txt +28 -0
- mongoforge_odm-0.1.0/mongoforge_odm.egg-info/dependency_links.txt +1 -0
- mongoforge_odm-0.1.0/mongoforge_odm.egg-info/requires.txt +7 -0
- mongoforge_odm-0.1.0/mongoforge_odm.egg-info/top_level.txt +1 -0
- mongoforge_odm-0.1.0/pyproject.toml +46 -0
- mongoforge_odm-0.1.0/setup.cfg +4 -0
|
@@ -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,204 @@
|
|
|
1
|
+
# 🔧 MongoForge
|
|
2
|
+
|
|
3
|
+
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.
|
|
4
|
+
|
|
5
|
+
Built on [PyMongo](https://pymongo.readthedocs.io/).
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## ✨ Features
|
|
10
|
+
|
|
11
|
+
- **Declarative schemas** — define documents as Python classes with typed fields
|
|
12
|
+
- **10 built-in field types** — `StringField`, `IntField`, `FloatField`, `BoolField`, `ListField`, `DateTimeField`, `DictField`, `ObjectIdField`, `EmbeddedField` + extensible `BaseField`
|
|
13
|
+
- **Rich validation** — required, min/max length, min/max value, regex, choices, custom validators
|
|
14
|
+
- **Metaclass magic** — automatic field collection, inheritance, collection naming
|
|
15
|
+
- **Manager API** — `create`, `bulk_create`, `all`, `first`, `get`, `filter`, `count`, `update`, `update_one`, `update_many`, `delete_one`, `delete_many`
|
|
16
|
+
- **Django-style filter operators** — `age__gte=18`, `name__in=[...]`, `status__ne="inactive"` (supports: `gt`, `gte`, `lt`, `lte`, `ne`, `in`, `nin`)
|
|
17
|
+
- **Auto timestamps** — `DateTimeField(auto_now=True)` and `auto_now_add=True`
|
|
18
|
+
- **Attribute access** — `user.name` instead of `user["name"]`
|
|
19
|
+
- **Serialization** — `to_mongo()` and `from_mongo()` for round-trip conversion
|
|
20
|
+
- **Abstract models & inheritance** — share fields across model hierarchies
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## 📦 Installation
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
# Clone the repository
|
|
28
|
+
git clone https://github.com/yourusername/MongoForge.git
|
|
29
|
+
cd MongoForge
|
|
30
|
+
|
|
31
|
+
# Create and activate a virtual environment
|
|
32
|
+
python -m venv env
|
|
33
|
+
env\Scripts\activate # Windows
|
|
34
|
+
# source env/bin/activate # macOS/Linux
|
|
35
|
+
|
|
36
|
+
# Install dependencies
|
|
37
|
+
pip install pymongo python-dotenv python-slugify
|
|
38
|
+
|
|
39
|
+
# For running tests (no live MongoDB needed)
|
|
40
|
+
pip install pytest mongomock
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Environment Setup
|
|
44
|
+
|
|
45
|
+
Create a `.env` file in the project root:
|
|
46
|
+
|
|
47
|
+
```env
|
|
48
|
+
MONGO_URI=mongodb://localhost:27017/
|
|
49
|
+
DATABASE_NAME=mongoforge
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## 🚀 Quick Start
|
|
55
|
+
|
|
56
|
+
### Define a Model
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
from mongoforge import MongoForge
|
|
60
|
+
from mongoforge.fields import StringField, IntField, BoolField, DateTimeField, ListField
|
|
61
|
+
|
|
62
|
+
class User(MongoForge):
|
|
63
|
+
name = StringField(required=True, min_length=2, max_length=100)
|
|
64
|
+
email = StringField(required=True, regex=r"^[\w.+-]+@[\w-]+\.[\w.-]+$")
|
|
65
|
+
age = IntField(min_value=0, max_value=150)
|
|
66
|
+
is_admin = BoolField(default=False)
|
|
67
|
+
tags = ListField(StringField())
|
|
68
|
+
created = DateTimeField(auto_now_add=True)
|
|
69
|
+
updated = DateTimeField(auto_now=True)
|
|
70
|
+
|
|
71
|
+
class Meta:
|
|
72
|
+
collection = "users"
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Create & Validate
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
user = User(name="Saurabh", email="saurabh@example.com", age=25)
|
|
79
|
+
|
|
80
|
+
# Validate before saving
|
|
81
|
+
user.validate() # raises ValidationError if invalid
|
|
82
|
+
|
|
83
|
+
# Serialize to MongoDB document
|
|
84
|
+
doc = user.to_mongo()
|
|
85
|
+
|
|
86
|
+
# Insert into database
|
|
87
|
+
User.objects.create(doc)
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Query
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
# Get all documents
|
|
94
|
+
User.objects.all() # → list[dict]
|
|
95
|
+
|
|
96
|
+
# Filter with operators
|
|
97
|
+
User.objects.filter(age__gte=18) # age >= 18
|
|
98
|
+
User.objects.filter(name__in=["Alice", "Bob"])
|
|
99
|
+
|
|
100
|
+
# Get single document
|
|
101
|
+
User.objects.get({"email": "saurabh@example.com"})
|
|
102
|
+
|
|
103
|
+
# Count
|
|
104
|
+
User.objects.count()
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### Update & Delete
|
|
108
|
+
|
|
109
|
+
```python
|
|
110
|
+
# Update by ID
|
|
111
|
+
User.objects.update(id="...", update_data={"name": "Updated Name"})
|
|
112
|
+
|
|
113
|
+
# Update many
|
|
114
|
+
User.objects.update_many({"age": {"$gte": 30}}, {"is_admin": True})
|
|
115
|
+
|
|
116
|
+
# Delete
|
|
117
|
+
User.objects.delete_one({"name": "Alice"})
|
|
118
|
+
User.objects.delete_many({"age": {"$lt": 18}})
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### Validation in Action
|
|
122
|
+
|
|
123
|
+
```python
|
|
124
|
+
from mongoforge.exceptions import ValidationError
|
|
125
|
+
|
|
126
|
+
user = User(name="A", email="not-an-email", age=-5)
|
|
127
|
+
try:
|
|
128
|
+
user.validate()
|
|
129
|
+
except ValidationError as exc:
|
|
130
|
+
print(exc)
|
|
131
|
+
# Validation failed. name: Minimum length is 2; email: Value does not match regex ...; age: Minimum value is 0
|
|
132
|
+
print(exc.errors)
|
|
133
|
+
# {'name': ['Minimum length is 2'], 'email': ['Value does not match regex ...'], 'age': ['Minimum value is 0']}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## 📋 Field Types
|
|
139
|
+
|
|
140
|
+
| Field | Python Type | Extra Options |
|
|
141
|
+
|---|---|---|
|
|
142
|
+
| `StringField` | `str` | `min_length`, `max_length`, `regex` |
|
|
143
|
+
| `IntField` | `int` | `min_value`, `max_value` |
|
|
144
|
+
| `FloatField` | `float` | — |
|
|
145
|
+
| `BoolField` | `bool` | — |
|
|
146
|
+
| `ListField` | `list` | `field` (inner field for item validation) |
|
|
147
|
+
| `DateTimeField` | `datetime` | `auto_now`, `auto_now_add` |
|
|
148
|
+
| `DictField` | `dict` | — |
|
|
149
|
+
| `ObjectIdField` | `ObjectId` | Auto-generates if `None` |
|
|
150
|
+
| `EmbeddedField` | `dict` | `document_class` (stub — Phase 4) |
|
|
151
|
+
|
|
152
|
+
**Common options** (all fields): `required`, `default`, `unique`, `choices`, `validators`, `null`
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
## 🏗️ Architecture
|
|
157
|
+
|
|
158
|
+
```
|
|
159
|
+
Model Class → MongoForgeMeta (metaclass) → collects fields + attaches MongoManager
|
|
160
|
+
│
|
|
161
|
+
┌─────────────────────────────────────┘
|
|
162
|
+
▼
|
|
163
|
+
MongoManager (cls.objects)
|
|
164
|
+
├── CreateMixin → create(), bulk_create()
|
|
165
|
+
├── ReadMixin → all(), first(), get(), filter(), count()
|
|
166
|
+
├── UpdateMixin → update(), update_one(), update_many()
|
|
167
|
+
└── DeleteMixin → delete_one(), delete_many()
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
---
|
|
171
|
+
|
|
172
|
+
## 🧪 Running Tests
|
|
173
|
+
|
|
174
|
+
Tests use [mongomock](https://github.com/mongomock/mongomock) — **no live MongoDB required**.
|
|
175
|
+
|
|
176
|
+
```bash
|
|
177
|
+
# With venv activated
|
|
178
|
+
python -m pytest mongoforge/tests/ -v
|
|
179
|
+
|
|
180
|
+
# Or using venv python directly
|
|
181
|
+
env\Scripts\python.exe -m pytest mongoforge/tests/ -v
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
**148 tests** covering:
|
|
185
|
+
- All 10 field types + validation paths
|
|
186
|
+
- Model instances, attribute access, serialization
|
|
187
|
+
- Metaclass, inheritance, abstract models
|
|
188
|
+
- Full CRUD operations + filter operators
|
|
189
|
+
- Exception hierarchy + formatting
|
|
190
|
+
|
|
191
|
+
---
|
|
192
|
+
|
|
193
|
+
## 🗺️ Roadmap
|
|
194
|
+
|
|
195
|
+
| Phase | Description | Status |
|
|
196
|
+
|---|---|---|
|
|
197
|
+
| **Phase 1** | Fields & Schema | ✅ ~95% |
|
|
198
|
+
| **Phase 2** | QuerySet (chainable queries) | ⬜ Not started |
|
|
199
|
+
| **Phase 3** | Model Lifecycle (`save()`, `delete()`, `reload()`, dirty tracking) | 🔶 ~50% |
|
|
200
|
+
| **Phase 4** | Relationships (EmbeddedDocument, ReferenceField) | ⬜ Not started |
|
|
201
|
+
| **Phase 5** | Indexes, Middleware & CLI | ⬜ Not started |
|
|
202
|
+
| **Phase 6** | Packaging & Developer Experience | 🔶 ~30% |
|
|
203
|
+
|
|
204
|
+
See [implementation_plan.md](implementation_plan.md) for the full roadmap with target APIs.
|
|
@@ -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
|