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 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
+ [![CI](https://github.com/OneTesseractInMultiverse/moleql/actions/workflows/ci.yml/badge.svg)](https://github.com/OneTesseractInMultiverse/moleql/actions)
23
+ [![License](https://img.shields.io/github/license/OneTesseractInMultiverse/moleql.svg)](LICENSE)
24
+ [![PyPI](https://img.shields.io/pypi/v/moleql.svg)](https://pypi.org/project/moleql)
25
+ [![Python Versions](https://img.shields.io/pypi/pyversions/moleql.svg)](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
+ [![CI](https://github.com/OneTesseractInMultiverse/moleql/actions/workflows/ci.yml/badge.svg)](https://github.com/OneTesseractInMultiverse/moleql/actions)
6
+ [![License](https://img.shields.io/github/license/OneTesseractInMultiverse/moleql.svg)](LICENSE)
7
+ [![PyPI](https://img.shields.io/pypi/v/moleql.svg)](https://pypi.org/project/moleql)
8
+ [![Python Versions](https://img.shields.io/pypi/pyversions/moleql.svg)](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,3 @@
1
+ from moleql.mql import MoleQL as MoleQL
2
+ from moleql.parse import moleqularize as moleqularize
3
+ from moleql.parse import parse as parse
@@ -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,3 @@
1
+ from moleql.mql.core import MoleQL
2
+
3
+ __all__ = ["MoleQL"]
@@ -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"