moleql 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.
- moleql-0.1.1/PKG-INFO +294 -0
- moleql-0.1.1/README.md +277 -0
- moleql-0.1.1/pyproject.toml +48 -0
- moleql-0.1.1/src/moleql/__init__.py +3 -0
- moleql-0.1.1/src/moleql/default_casters.py +20 -0
- moleql-0.1.1/src/moleql/mql/__init__.py +3 -0
- moleql-0.1.1/src/moleql/mql/casters.py +44 -0
- moleql-0.1.1/src/moleql/mql/constants.py +10 -0
- moleql-0.1.1/src/moleql/mql/core.py +299 -0
- moleql-0.1.1/src/moleql/mql/date_parser.py +16 -0
- moleql-0.1.1/src/moleql/mql/errors.py +38 -0
- moleql-0.1.1/src/moleql/mql/filter_handler.py +200 -0
- moleql-0.1.1/src/moleql/mql/limit_skip_handler.py +63 -0
- moleql-0.1.1/src/moleql/mql/operator_mapping.py +12 -0
- moleql-0.1.1/src/moleql/mql/projection_handler.py +98 -0
- moleql-0.1.1/src/moleql/mql/regex_parser.py +50 -0
- moleql-0.1.1/src/moleql/mql/sort_handler.py +54 -0
- moleql-0.1.1/src/moleql/mql/text_search_handler.py +21 -0
- moleql-0.1.1/src/moleql/mql/type_mapping.py +29 -0
- moleql-0.1.1/src/moleql/parse.py +95 -0
- moleql-0.1.1/src/moleql/py.typed +0 -0
moleql-0.1.1/PKG-INFO
ADDED
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: moleql
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Add your description here
|
|
5
|
+
Author: Pedro Guzmán
|
|
6
|
+
Author-email: Pedro Guzmán <pedro@subvertic.com>
|
|
7
|
+
Requires-Dist: dateparse>=1.4.0
|
|
8
|
+
Requires-Dist: dateparser>=1.2.2
|
|
9
|
+
Requires-Dist: mypy>=1.18.2
|
|
10
|
+
Requires-Dist: pymongo>=4.15.3
|
|
11
|
+
Requires-Dist: pytest>=8.4.2
|
|
12
|
+
Requires-Dist: ruff>=0.14.4
|
|
13
|
+
Requires-Python: >=3.12
|
|
14
|
+
Project-URL: Homepage, https://github.com/OneTesseractInMultiverse/moleql
|
|
15
|
+
Project-URL: Issues, https://github.com/OneTesseractInMultiverse/moleql/issues
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
|
|
18
|
+
# MoleQL
|
|
19
|
+
|
|
20
|
+
> Expressive URL-query to MongoDB query conversion library.
|
|
21
|
+
|
|
22
|
+
[](https://github.com/OneTesseractInMultiverse/moleql/actions)
|
|
23
|
+
[](LICENSE)
|
|
24
|
+
[](https://pypi.org/project/moleql)
|
|
25
|
+
[](https://pypi.org/project/moleql)
|
|
26
|
+
|
|
27
|
+
## Overview
|
|
28
|
+
|
|
29
|
+
MoleQL allows your REST APIs or data layers to accept
|
|
30
|
+
URL-style query strings (e.g. `?age>30&status=in(active,pending)`)
|
|
31
|
+
and converts them into MongoDB-compatible query dictionaries.
|
|
32
|
+
It supports filters, sorting, skip/limit, projection and text search.
|
|
33
|
+
|
|
34
|
+
Example:
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
from moleql import parse
|
|
38
|
+
|
|
39
|
+
query = parse("age>30&country=US&name=/^John/i")
|
|
40
|
+
# yields:
|
|
41
|
+
# {
|
|
42
|
+
# "filter": {"age": {"$gt": 30}, "country": "US", "name": {"$regex": "^John", "$options": "i"}},
|
|
43
|
+
# "sort": None,
|
|
44
|
+
# "skip": 0,
|
|
45
|
+
# "limit": 0,
|
|
46
|
+
# "projection": None
|
|
47
|
+
# }
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## ✨ Features
|
|
51
|
+
|
|
52
|
+
- 🧠 Intuitive syntax: `=, !=, <, <=, >, >=`
|
|
53
|
+
- 📋 List operators: `in(...), ...`
|
|
54
|
+
- 🔍 Regex: `/pattern/flags → $regex + $options`
|
|
55
|
+
- 🧾 Pagination: `skip=10, limit=50`
|
|
56
|
+
- ⚙️ Sorting: `sort=-created_at,name`
|
|
57
|
+
- 🧩 Projection: `fields=name,email,age`
|
|
58
|
+
- 🔠 Text search: `text=free text here`
|
|
59
|
+
- 🧱 Type casters: `custom value transformations`
|
|
60
|
+
- 🚫 Blacklist: skip parsing of restricted fields
|
|
61
|
+
- 🧪 Tested: full pytest coverage, Ruff linting
|
|
62
|
+
|
|
63
|
+
## 🧰 Installation
|
|
64
|
+
|
|
65
|
+
With uv (recommended):
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
uv add moleql
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
pip install moleql
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## 🚀 Quick Start
|
|
76
|
+
|
|
77
|
+
from moleql import parse
|
|
78
|
+
|
|
79
|
+
query = parse("age>30&country=US&name=/^John/i")
|
|
80
|
+
print(query)
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
from moleql import parse
|
|
84
|
+
|
|
85
|
+
query = parse("age>30&country=US&name=/^John/i")
|
|
86
|
+
print(query)
|
|
87
|
+
|
|
88
|
+
# {
|
|
89
|
+
# "filter": {
|
|
90
|
+
# "age": {"$gt": 30},
|
|
91
|
+
# "country": "US",
|
|
92
|
+
# "name": {"$regex": "^John", "$options": "i"}
|
|
93
|
+
# },
|
|
94
|
+
# "sort": None,
|
|
95
|
+
# "skip": 0,
|
|
96
|
+
# "limit": 0,
|
|
97
|
+
# "projection": None
|
|
98
|
+
# }
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Extended example
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
from moleql import parse
|
|
105
|
+
|
|
106
|
+
query = parse(
|
|
107
|
+
"age>=18&status=in(active,pending)"
|
|
108
|
+
"&sort=-created_at,name"
|
|
109
|
+
"&skip=10&limit=20"
|
|
110
|
+
"&fields=name,email,age"
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Output:
|
|
116
|
+
|
|
117
|
+
```python
|
|
118
|
+
{
|
|
119
|
+
"filter": {
|
|
120
|
+
"age": {"$gte": 18},
|
|
121
|
+
"status": {"$in": ["active", "pending"]}
|
|
122
|
+
},
|
|
123
|
+
"sort": {"created_at": -1, "name": 1},
|
|
124
|
+
"skip": 10,
|
|
125
|
+
"limit": 20,
|
|
126
|
+
"projection": {"name": 1, "email": 1, "age": 1}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## 🔣 Supported Operators
|
|
132
|
+
|
|
133
|
+
| Expression | Mongo Operator | Example |
|
|
134
|
+
|-----------------:|:---------------|:--------------------------|
|
|
135
|
+
| `=` | direct match | `age=20` → `{"age": 20}` |
|
|
136
|
+
| `!=` | `$ne` | `status!=active` |
|
|
137
|
+
| `>` / `>=` | `$gt` / `$gte` | `score>=80` |
|
|
138
|
+
| `<` / `<=` | `$lt` / `$lte` | `price<10` |
|
|
139
|
+
| `in(...)` | `$in` | `role=in(admin,user)` |
|
|
140
|
+
| `!=in(...)` | `$nin` | `tier!=in(gold,platinum)` |
|
|
141
|
+
| `/pattern/flags` | `$regex` | `name=/^Jo/i` |
|
|
142
|
+
| `text=` | `$text` | `text=free search text` |
|
|
143
|
+
| `fields=` | projection | `fields=name,email` |
|
|
144
|
+
| `sort=` | sort directive | `sort=-created_at,name` |
|
|
145
|
+
|
|
146
|
+
## 🧱 Quick API Reference
|
|
147
|
+
|
|
148
|
+
`parse(moleql_query: str, blacklist=None, casters=None) -> dict`
|
|
149
|
+
|
|
150
|
+
```python
|
|
151
|
+
from moleql import parse
|
|
152
|
+
|
|
153
|
+
parse("age>25&active=true")
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Returns a dictionary with:
|
|
157
|
+
`filter`, `sort`, `skip`, `limit`, `projection`
|
|
158
|
+
|
|
159
|
+
`moleqularize(moleql_query: str, blacklist=None, casters=None) -> MoleQL`
|
|
160
|
+
|
|
161
|
+
Parse a MoleQL string and return the internal MoleQL object for
|
|
162
|
+
advanced inspection and debugging.
|
|
163
|
+
|
|
164
|
+
```python
|
|
165
|
+
from moleql import moleqularize
|
|
166
|
+
|
|
167
|
+
m = moleqularize("age>25")
|
|
168
|
+
print(m.mongo_query)
|
|
169
|
+
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## 🧩 Custom Casters
|
|
173
|
+
|
|
174
|
+
You can define custom casters to control type conversions.
|
|
175
|
+
|
|
176
|
+
```python
|
|
177
|
+
from moleql import parse, get_casters
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def to_bool(value: str) -> bool:
|
|
181
|
+
return value.lower() in ("true", "1", "yes")
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
custom_casters = {"bool": to_bool}
|
|
185
|
+
|
|
186
|
+
q = parse("active=bool(true)&age>30", casters=custom_casters)
|
|
187
|
+
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
## 🧠 Design Philosophy
|
|
191
|
+
|
|
192
|
+
- Transparency — The query string is readable and reversible.
|
|
193
|
+
- Safety — No eval, injection-safe parsing.
|
|
194
|
+
- Extensibility — Easy to plug in custom handlers.
|
|
195
|
+
- Predictability — Every operator maps 1:1 to Mongo semantics.
|
|
196
|
+
- Minimal dependencies — Pure Python, no ODM required.
|
|
197
|
+
|
|
198
|
+
## ⚙️ Integration Examples
|
|
199
|
+
|
|
200
|
+
**FastAPI**
|
|
201
|
+
|
|
202
|
+
```python
|
|
203
|
+
from fastapi import FastAPI, Request
|
|
204
|
+
from moleql import parse
|
|
205
|
+
from pymongo import MongoClient
|
|
206
|
+
|
|
207
|
+
db = MongoClient()["app"]
|
|
208
|
+
app = FastAPI()
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
@app.get("/users")
|
|
212
|
+
def list_users(request: Request):
|
|
213
|
+
q = parse(request.url.query.lstrip("?"))
|
|
214
|
+
return list(db.users.find(q["filter"]))
|
|
215
|
+
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
**Flask**
|
|
219
|
+
|
|
220
|
+
```python
|
|
221
|
+
from flask import Flask, request, jsonify
|
|
222
|
+
from moleql import parse
|
|
223
|
+
|
|
224
|
+
app = Flask(__name__)
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
@app.get("/orders")
|
|
228
|
+
def orders():
|
|
229
|
+
q = parse(request.query_string.decode())
|
|
230
|
+
return jsonify(q)
|
|
231
|
+
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
## 🧪 Testing
|
|
235
|
+
|
|
236
|
+
Run the full suite using uv:
|
|
237
|
+
|
|
238
|
+
```bash
|
|
239
|
+
uv sync --all-extras --dev
|
|
240
|
+
uv run pytest -vv
|
|
241
|
+
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
Generate coverage:
|
|
245
|
+
|
|
246
|
+
```bash
|
|
247
|
+
uv run pytest --cov=moleql --cov-report=term-missing
|
|
248
|
+
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
## 🧹 Code Quality
|
|
252
|
+
|
|
253
|
+
This repository uses:
|
|
254
|
+
|
|
255
|
+
- Ruff — Linting + formatting (UP / pyupgrade rules)
|
|
256
|
+
- pre-commit — automatic checks
|
|
257
|
+
- pytest — testing
|
|
258
|
+
- uv — environment & packaging
|
|
259
|
+
|
|
260
|
+
Set up hooks:
|
|
261
|
+
```bash
|
|
262
|
+
uv run pre-commit install
|
|
263
|
+
uv run pre-commit run --all-files
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
## 🧭 Roadmap
|
|
267
|
+
|
|
268
|
+
- [] Add exists(field) and between(a,b) operators
|
|
269
|
+
- [] Add alias for text= → $search convenience
|
|
270
|
+
- [] Add optional CLI (moleql "age>20" --as-json)
|
|
271
|
+
- [] Extended docs and tutorials
|
|
272
|
+
|
|
273
|
+
🤝 Contributing
|
|
274
|
+
|
|
275
|
+
1. Fork this repository
|
|
276
|
+
2. Create your feature branch
|
|
277
|
+
|
|
278
|
+
```bash
|
|
279
|
+
git checkout -b feat/awesome-change
|
|
280
|
+
```
|
|
281
|
+
3. Run formatting and tests
|
|
282
|
+
|
|
283
|
+
```bash
|
|
284
|
+
uv run pre-commit run --all-files
|
|
285
|
+
uv run pytest
|
|
286
|
+
```
|
|
287
|
+
4. Commit and push your changes
|
|
288
|
+
5. Open a Pull Request 🚀
|
|
289
|
+
|
|
290
|
+
## 🌟 Acknowledgments
|
|
291
|
+
|
|
292
|
+
Built and maintained by [@OneTesseractInMultiverse](https://github.com/OneTesseractInMultiverse)
|
|
293
|
+
Inspired by the need for readable, typed, and safe Mongo queries in API
|
|
294
|
+
environments.
|
moleql-0.1.1/README.md
ADDED
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
# MoleQL
|
|
2
|
+
|
|
3
|
+
> Expressive URL-query to MongoDB query conversion library.
|
|
4
|
+
|
|
5
|
+
[](https://github.com/OneTesseractInMultiverse/moleql/actions)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
[](https://pypi.org/project/moleql)
|
|
8
|
+
[](https://pypi.org/project/moleql)
|
|
9
|
+
|
|
10
|
+
## Overview
|
|
11
|
+
|
|
12
|
+
MoleQL allows your REST APIs or data layers to accept
|
|
13
|
+
URL-style query strings (e.g. `?age>30&status=in(active,pending)`)
|
|
14
|
+
and converts them into MongoDB-compatible query dictionaries.
|
|
15
|
+
It supports filters, sorting, skip/limit, projection and text search.
|
|
16
|
+
|
|
17
|
+
Example:
|
|
18
|
+
|
|
19
|
+
```python
|
|
20
|
+
from moleql import parse
|
|
21
|
+
|
|
22
|
+
query = parse("age>30&country=US&name=/^John/i")
|
|
23
|
+
# yields:
|
|
24
|
+
# {
|
|
25
|
+
# "filter": {"age": {"$gt": 30}, "country": "US", "name": {"$regex": "^John", "$options": "i"}},
|
|
26
|
+
# "sort": None,
|
|
27
|
+
# "skip": 0,
|
|
28
|
+
# "limit": 0,
|
|
29
|
+
# "projection": None
|
|
30
|
+
# }
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## ✨ Features
|
|
34
|
+
|
|
35
|
+
- 🧠 Intuitive syntax: `=, !=, <, <=, >, >=`
|
|
36
|
+
- 📋 List operators: `in(...), ...`
|
|
37
|
+
- 🔍 Regex: `/pattern/flags → $regex + $options`
|
|
38
|
+
- 🧾 Pagination: `skip=10, limit=50`
|
|
39
|
+
- ⚙️ Sorting: `sort=-created_at,name`
|
|
40
|
+
- 🧩 Projection: `fields=name,email,age`
|
|
41
|
+
- 🔠 Text search: `text=free text here`
|
|
42
|
+
- 🧱 Type casters: `custom value transformations`
|
|
43
|
+
- 🚫 Blacklist: skip parsing of restricted fields
|
|
44
|
+
- 🧪 Tested: full pytest coverage, Ruff linting
|
|
45
|
+
|
|
46
|
+
## 🧰 Installation
|
|
47
|
+
|
|
48
|
+
With uv (recommended):
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
uv add moleql
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
pip install moleql
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## 🚀 Quick Start
|
|
59
|
+
|
|
60
|
+
from moleql import parse
|
|
61
|
+
|
|
62
|
+
query = parse("age>30&country=US&name=/^John/i")
|
|
63
|
+
print(query)
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
from moleql import parse
|
|
67
|
+
|
|
68
|
+
query = parse("age>30&country=US&name=/^John/i")
|
|
69
|
+
print(query)
|
|
70
|
+
|
|
71
|
+
# {
|
|
72
|
+
# "filter": {
|
|
73
|
+
# "age": {"$gt": 30},
|
|
74
|
+
# "country": "US",
|
|
75
|
+
# "name": {"$regex": "^John", "$options": "i"}
|
|
76
|
+
# },
|
|
77
|
+
# "sort": None,
|
|
78
|
+
# "skip": 0,
|
|
79
|
+
# "limit": 0,
|
|
80
|
+
# "projection": None
|
|
81
|
+
# }
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Extended example
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
from moleql import parse
|
|
88
|
+
|
|
89
|
+
query = parse(
|
|
90
|
+
"age>=18&status=in(active,pending)"
|
|
91
|
+
"&sort=-created_at,name"
|
|
92
|
+
"&skip=10&limit=20"
|
|
93
|
+
"&fields=name,email,age"
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Output:
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
{
|
|
102
|
+
"filter": {
|
|
103
|
+
"age": {"$gte": 18},
|
|
104
|
+
"status": {"$in": ["active", "pending"]}
|
|
105
|
+
},
|
|
106
|
+
"sort": {"created_at": -1, "name": 1},
|
|
107
|
+
"skip": 10,
|
|
108
|
+
"limit": 20,
|
|
109
|
+
"projection": {"name": 1, "email": 1, "age": 1}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## 🔣 Supported Operators
|
|
115
|
+
|
|
116
|
+
| Expression | Mongo Operator | Example |
|
|
117
|
+
|-----------------:|:---------------|:--------------------------|
|
|
118
|
+
| `=` | direct match | `age=20` → `{"age": 20}` |
|
|
119
|
+
| `!=` | `$ne` | `status!=active` |
|
|
120
|
+
| `>` / `>=` | `$gt` / `$gte` | `score>=80` |
|
|
121
|
+
| `<` / `<=` | `$lt` / `$lte` | `price<10` |
|
|
122
|
+
| `in(...)` | `$in` | `role=in(admin,user)` |
|
|
123
|
+
| `!=in(...)` | `$nin` | `tier!=in(gold,platinum)` |
|
|
124
|
+
| `/pattern/flags` | `$regex` | `name=/^Jo/i` |
|
|
125
|
+
| `text=` | `$text` | `text=free search text` |
|
|
126
|
+
| `fields=` | projection | `fields=name,email` |
|
|
127
|
+
| `sort=` | sort directive | `sort=-created_at,name` |
|
|
128
|
+
|
|
129
|
+
## 🧱 Quick API Reference
|
|
130
|
+
|
|
131
|
+
`parse(moleql_query: str, blacklist=None, casters=None) -> dict`
|
|
132
|
+
|
|
133
|
+
```python
|
|
134
|
+
from moleql import parse
|
|
135
|
+
|
|
136
|
+
parse("age>25&active=true")
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Returns a dictionary with:
|
|
140
|
+
`filter`, `sort`, `skip`, `limit`, `projection`
|
|
141
|
+
|
|
142
|
+
`moleqularize(moleql_query: str, blacklist=None, casters=None) -> MoleQL`
|
|
143
|
+
|
|
144
|
+
Parse a MoleQL string and return the internal MoleQL object for
|
|
145
|
+
advanced inspection and debugging.
|
|
146
|
+
|
|
147
|
+
```python
|
|
148
|
+
from moleql import moleqularize
|
|
149
|
+
|
|
150
|
+
m = moleqularize("age>25")
|
|
151
|
+
print(m.mongo_query)
|
|
152
|
+
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## 🧩 Custom Casters
|
|
156
|
+
|
|
157
|
+
You can define custom casters to control type conversions.
|
|
158
|
+
|
|
159
|
+
```python
|
|
160
|
+
from moleql import parse, get_casters
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def to_bool(value: str) -> bool:
|
|
164
|
+
return value.lower() in ("true", "1", "yes")
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
custom_casters = {"bool": to_bool}
|
|
168
|
+
|
|
169
|
+
q = parse("active=bool(true)&age>30", casters=custom_casters)
|
|
170
|
+
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
## 🧠 Design Philosophy
|
|
174
|
+
|
|
175
|
+
- Transparency — The query string is readable and reversible.
|
|
176
|
+
- Safety — No eval, injection-safe parsing.
|
|
177
|
+
- Extensibility — Easy to plug in custom handlers.
|
|
178
|
+
- Predictability — Every operator maps 1:1 to Mongo semantics.
|
|
179
|
+
- Minimal dependencies — Pure Python, no ODM required.
|
|
180
|
+
|
|
181
|
+
## ⚙️ Integration Examples
|
|
182
|
+
|
|
183
|
+
**FastAPI**
|
|
184
|
+
|
|
185
|
+
```python
|
|
186
|
+
from fastapi import FastAPI, Request
|
|
187
|
+
from moleql import parse
|
|
188
|
+
from pymongo import MongoClient
|
|
189
|
+
|
|
190
|
+
db = MongoClient()["app"]
|
|
191
|
+
app = FastAPI()
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
@app.get("/users")
|
|
195
|
+
def list_users(request: Request):
|
|
196
|
+
q = parse(request.url.query.lstrip("?"))
|
|
197
|
+
return list(db.users.find(q["filter"]))
|
|
198
|
+
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
**Flask**
|
|
202
|
+
|
|
203
|
+
```python
|
|
204
|
+
from flask import Flask, request, jsonify
|
|
205
|
+
from moleql import parse
|
|
206
|
+
|
|
207
|
+
app = Flask(__name__)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
@app.get("/orders")
|
|
211
|
+
def orders():
|
|
212
|
+
q = parse(request.query_string.decode())
|
|
213
|
+
return jsonify(q)
|
|
214
|
+
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
## 🧪 Testing
|
|
218
|
+
|
|
219
|
+
Run the full suite using uv:
|
|
220
|
+
|
|
221
|
+
```bash
|
|
222
|
+
uv sync --all-extras --dev
|
|
223
|
+
uv run pytest -vv
|
|
224
|
+
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
Generate coverage:
|
|
228
|
+
|
|
229
|
+
```bash
|
|
230
|
+
uv run pytest --cov=moleql --cov-report=term-missing
|
|
231
|
+
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
## 🧹 Code Quality
|
|
235
|
+
|
|
236
|
+
This repository uses:
|
|
237
|
+
|
|
238
|
+
- Ruff — Linting + formatting (UP / pyupgrade rules)
|
|
239
|
+
- pre-commit — automatic checks
|
|
240
|
+
- pytest — testing
|
|
241
|
+
- uv — environment & packaging
|
|
242
|
+
|
|
243
|
+
Set up hooks:
|
|
244
|
+
```bash
|
|
245
|
+
uv run pre-commit install
|
|
246
|
+
uv run pre-commit run --all-files
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
## 🧭 Roadmap
|
|
250
|
+
|
|
251
|
+
- [] Add exists(field) and between(a,b) operators
|
|
252
|
+
- [] Add alias for text= → $search convenience
|
|
253
|
+
- [] Add optional CLI (moleql "age>20" --as-json)
|
|
254
|
+
- [] Extended docs and tutorials
|
|
255
|
+
|
|
256
|
+
🤝 Contributing
|
|
257
|
+
|
|
258
|
+
1. Fork this repository
|
|
259
|
+
2. Create your feature branch
|
|
260
|
+
|
|
261
|
+
```bash
|
|
262
|
+
git checkout -b feat/awesome-change
|
|
263
|
+
```
|
|
264
|
+
3. Run formatting and tests
|
|
265
|
+
|
|
266
|
+
```bash
|
|
267
|
+
uv run pre-commit run --all-files
|
|
268
|
+
uv run pytest
|
|
269
|
+
```
|
|
270
|
+
4. Commit and push your changes
|
|
271
|
+
5. Open a Pull Request 🚀
|
|
272
|
+
|
|
273
|
+
## 🌟 Acknowledgments
|
|
274
|
+
|
|
275
|
+
Built and maintained by [@OneTesseractInMultiverse](https://github.com/OneTesseractInMultiverse)
|
|
276
|
+
Inspired by the need for readable, typed, and safe Mongo queries in API
|
|
277
|
+
environments.
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "moleql"
|
|
3
|
+
version = "0.1.1"
|
|
4
|
+
description = "Add your description here"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
authors = [
|
|
7
|
+
{ name = "Pedro Guzmán", email = "pedro@subvertic.com" }
|
|
8
|
+
]
|
|
9
|
+
requires-python = ">=3.12"
|
|
10
|
+
dependencies = [
|
|
11
|
+
"dateparse>=1.4.0",
|
|
12
|
+
"dateparser>=1.2.2",
|
|
13
|
+
"mypy>=1.18.2",
|
|
14
|
+
"pymongo>=4.15.3",
|
|
15
|
+
"pytest>=8.4.2",
|
|
16
|
+
"ruff>=0.14.4",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
[project.urls]
|
|
20
|
+
Homepage = "https://github.com/OneTesseractInMultiverse/moleql"
|
|
21
|
+
Issues = "https://github.com/OneTesseractInMultiverse/moleql/issues"
|
|
22
|
+
|
|
23
|
+
[build-system]
|
|
24
|
+
requires = ["uv_build>=0.8.23,<0.9.0"]
|
|
25
|
+
build-backend = "uv_build"
|
|
26
|
+
|
|
27
|
+
[tool.ruff]
|
|
28
|
+
line-length = 100
|
|
29
|
+
src = ["src", "tests"]
|
|
30
|
+
|
|
31
|
+
[tool.ruff.lint]
|
|
32
|
+
select = ["F", "E", "W", "I", "UP", "PERF"]
|
|
33
|
+
ignore = [] # add codes you want to silence
|
|
34
|
+
|
|
35
|
+
[tool.ruff.format]
|
|
36
|
+
|
|
37
|
+
[tool.hatch.build.include]
|
|
38
|
+
paths = ["LICENSE", "NOTICE", "AUTHORS"]
|
|
39
|
+
|
|
40
|
+
[dependency-groups]
|
|
41
|
+
dev = [
|
|
42
|
+
"pre-commit>=4.3.0",
|
|
43
|
+
"pyinstrument>=5.1.1",
|
|
44
|
+
"pytest>=8.4.2",
|
|
45
|
+
"pytest-benchmark>=5.2.2",
|
|
46
|
+
"pytest-cov>=7.0.0",
|
|
47
|
+
"ruff>=0.14.4",
|
|
48
|
+
]
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from collections.abc import Callable
|
|
2
|
+
|
|
3
|
+
from moleql.mql.casters import (
|
|
4
|
+
cast_as_list,
|
|
5
|
+
cast_as_object_id,
|
|
6
|
+
cast_as_object_id_ts,
|
|
7
|
+
cast_as_str,
|
|
8
|
+
cast_as_timestamp,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
MONGO_MODEL_ID_KEY: str = "mongo_id"
|
|
12
|
+
MONGO_INTERNAL_ID_KEY: str = "_id"
|
|
13
|
+
DEFAULT_HQL_CASTERS: dict[str, Callable] = {
|
|
14
|
+
"list": cast_as_list,
|
|
15
|
+
"in": cast_as_list,
|
|
16
|
+
"object_id": cast_as_object_id,
|
|
17
|
+
"object_id_ts": cast_as_object_id_ts,
|
|
18
|
+
"ts": cast_as_timestamp,
|
|
19
|
+
"str": cast_as_str,
|
|
20
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
|
|
3
|
+
from bson import ObjectId
|
|
4
|
+
|
|
5
|
+
LIST_DEFAULT_SEPARATOR: str = ","
|
|
6
|
+
EMPTY_STRING: str = ""
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
# ---------------------------------------------------------
|
|
10
|
+
# CAST AS LIST
|
|
11
|
+
# ---------------------------------------------------------
|
|
12
|
+
def cast_as_list(value: str) -> list[str]:
|
|
13
|
+
output_list: list[str] = value.strip().split(LIST_DEFAULT_SEPARATOR)
|
|
14
|
+
if output_list == [EMPTY_STRING]:
|
|
15
|
+
return []
|
|
16
|
+
return output_list
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# ---------------------------------------------------------
|
|
20
|
+
# CAST AS OBJECT ID
|
|
21
|
+
# ---------------------------------------------------------
|
|
22
|
+
def cast_as_object_id(value: str) -> ObjectId:
|
|
23
|
+
return ObjectId(value)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# ---------------------------------------------------------
|
|
27
|
+
# CAST AS OBJECT ID TS
|
|
28
|
+
# ---------------------------------------------------------
|
|
29
|
+
def cast_as_object_id_ts(value: str) -> datetime.datetime:
|
|
30
|
+
return ObjectId(value).generation_time
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# ---------------------------------------------------------
|
|
34
|
+
# CAST AS OBJECT ID
|
|
35
|
+
# ---------------------------------------------------------
|
|
36
|
+
def cast_as_timestamp(value: str) -> datetime.datetime:
|
|
37
|
+
return datetime.datetime.fromtimestamp(int(value))
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
# ---------------------------------------------------------
|
|
41
|
+
# CAST AS OBJECT ID
|
|
42
|
+
# ---------------------------------------------------------
|
|
43
|
+
def cast_as_str(value: any) -> str:
|
|
44
|
+
return str(value)
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
FILTERS_KEY: str = "filters"
|
|
2
|
+
SORT_KEY: str = "sort"
|
|
3
|
+
SKIP_KEY: str = "skip"
|
|
4
|
+
LIMIT_KEY: str = "limit"
|
|
5
|
+
PROJECTION_KEY: str = "projection"
|
|
6
|
+
QUERY_STRING_PARAM_SEPARATOR: str = "&"
|
|
7
|
+
EMPTY_STRING: str = ""
|
|
8
|
+
FIELDS_KEY: str = "fields"
|
|
9
|
+
TEXT_KEY: str = "$text"
|
|
10
|
+
FILTER: str = "filter"
|