pythia-mongo 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.
- pythia_mongo/__init__.py +5 -0
- pythia_mongo/__main__.py +5 -0
- pythia_mongo/catalog.py +124 -0
- pythia_mongo/cli.py +8 -0
- pythia_mongo/commands/collection.json +16 -0
- pythia_mongo/commands/collections.json +29 -0
- pythia_mongo/commands/connection-status.json +8 -0
- pythia_mongo/commands/count.json +12 -0
- pythia_mongo/commands/duplicates.json +43 -0
- pythia_mongo/commands/indexes.json +12 -0
- pythia_mongo/commands/invalid.json +45 -0
- pythia_mongo/commands/pipeline-run.json +18 -0
- pythia_mongo/commands/probe-sample.json +28 -0
- pythia_mongo/commands/probe.json +37 -0
- pythia_mongo/commands/sample.json +23 -0
- pythia_mongo/commands/server-build.json +9 -0
- pythia_mongo/commands/server-hello.json +9 -0
- pythia_mongo/commands/view-run.json +24 -0
- pythia_mongo/engine.py +301 -0
- pythia_mongo/guard.py +58 -0
- pythia_mongo/read.py +346 -0
- pythia_mongo/rules.py +132 -0
- pythia_mongo/skills/pythia-mongo-apply/SKILL.md +101 -0
- pythia_mongo/skills/pythia-mongo-apply/reference/refusals.md +31 -0
- pythia_mongo/skills/pythia-mongo-autopilot/SKILL.md +62 -0
- pythia_mongo/skills/pythia-mongo-deploy/SKILL.md +66 -0
- pythia_mongo/skills/pythia-mongo-explore/SKILL.md +65 -0
- pythia_mongo/skills/pythia-mongo-impact/SKILL.md +60 -0
- pythia_mongo/skills/pythia-mongo-verify/SKILL.md +58 -0
- pythia_mongo/skills/using-pythia-mongo/SKILL.md +68 -0
- pythia_mongo/write.py +714 -0
- pythia_mongo-0.1.0.dist-info/METADATA +395 -0
- pythia_mongo-0.1.0.dist-info/RECORD +37 -0
- pythia_mongo-0.1.0.dist-info/WHEEL +5 -0
- pythia_mongo-0.1.0.dist-info/entry_points.txt +2 -0
- pythia_mongo-0.1.0.dist-info/licenses/LICENSE +21 -0
- pythia_mongo-0.1.0.dist-info/top_level.txt +1 -0
pythia_mongo/__init__.py
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
"""pythia for MongoDB: database development for AI agents, over pythia-core.
|
|
2
|
+
Alpha — see README.md for what works today."""
|
|
3
|
+
__version__ = "0.1.0" # kept equal to pyproject.toml (tests/test_mongo.py)
|
|
4
|
+
|
|
5
|
+
from .engine import MongoEngine # noqa: E402,F401 — needs __version__ first
|
pythia_mongo/__main__.py
ADDED
pythia_mongo/catalog.py
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""Where pythia-mongo's commands live and how they are run.
|
|
2
|
+
|
|
3
|
+
MongoDB takes command documents, not SQL. Every command pythia-mongo sends
|
|
4
|
+
is a file in commands/, under the contract the SQL engines keep for their
|
|
5
|
+
query files: what it is for, the binds it takes, what it returns, and
|
|
6
|
+
nothing else — reviewable in isolation. A bind is written {"$bind": "name"}
|
|
7
|
+
where its value goes, so text from a person or an agent always lands as a
|
|
8
|
+
value, never as an operator or a command of its own.
|
|
9
|
+
"""
|
|
10
|
+
import json
|
|
11
|
+
import pathlib
|
|
12
|
+
import re
|
|
13
|
+
import sys
|
|
14
|
+
|
|
15
|
+
COMMAND_DIR = pathlib.Path(__file__).resolve().parent / "commands"
|
|
16
|
+
|
|
17
|
+
COMMAND_BINDS = {
|
|
18
|
+
"server-build.json": set(),
|
|
19
|
+
"server-hello.json": set(),
|
|
20
|
+
"collections.json": {"p"},
|
|
21
|
+
"collection.json": {"n"},
|
|
22
|
+
"indexes.json": {"n"},
|
|
23
|
+
"sample.json": {"n", "k"},
|
|
24
|
+
"count.json": {"n"},
|
|
25
|
+
"invalid.json": {"n", "v"},
|
|
26
|
+
"view-run.json": {"n", "k"},
|
|
27
|
+
"probe.json": {"n", "f"},
|
|
28
|
+
"probe-sample.json": {"n", "f", "k"},
|
|
29
|
+
"duplicates.json": {"n", "f", "g"},
|
|
30
|
+
"pipeline-run.json": {"n", "p"},
|
|
31
|
+
"connection-status.json": set(),
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
# every command file reads: never a write, a $out or a $merge (the suite
|
|
35
|
+
# checks). A statement's own command is not a file: it is the developer's.
|
|
36
|
+
READ_COMMANDS = frozenset(COMMAND_BINDS)
|
|
37
|
+
|
|
38
|
+
# commands whose answer is a cursor to read to the end
|
|
39
|
+
CURSOR_COMMANDS = ("listCollections", "listIndexes", "aggregate", "find")
|
|
40
|
+
|
|
41
|
+
_CACHE = {}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _load(name):
|
|
45
|
+
if name not in _CACHE:
|
|
46
|
+
path = COMMAND_DIR / name
|
|
47
|
+
if name not in COMMAND_BINDS or not path.is_file():
|
|
48
|
+
sys.exit(f"No command file {name} in {COMMAND_DIR}.")
|
|
49
|
+
_CACHE[name] = json.loads(path.read_text(encoding="utf-8"))
|
|
50
|
+
return _CACHE[name]
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def placeholders(value):
|
|
54
|
+
"""Every bind name a command document uses."""
|
|
55
|
+
if isinstance(value, dict):
|
|
56
|
+
if set(value) == {"$bind"}:
|
|
57
|
+
return {value["$bind"]}
|
|
58
|
+
return set().union(*(placeholders(v) for v in value.values())) if value else set()
|
|
59
|
+
if isinstance(value, list):
|
|
60
|
+
return set().union(*(placeholders(v) for v in value)) if value else set()
|
|
61
|
+
return set()
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _fill(value, binds):
|
|
65
|
+
if isinstance(value, dict):
|
|
66
|
+
if set(value) == {"$bind"}:
|
|
67
|
+
return binds[value["$bind"]]
|
|
68
|
+
return {k: _fill(v, binds) for k, v in value.items()}
|
|
69
|
+
if isinstance(value, list):
|
|
70
|
+
return [_fill(v, binds) for v in value]
|
|
71
|
+
return value
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def command_of(name, **binds):
|
|
75
|
+
"""The command document of one file, its binds in place. A bind the
|
|
76
|
+
file declares and the caller left out, or one it does not declare, is
|
|
77
|
+
refused rather than guessed at."""
|
|
78
|
+
doc = _load(name)
|
|
79
|
+
want = COMMAND_BINDS[name]
|
|
80
|
+
if set(binds) != want:
|
|
81
|
+
gap = sorted(want - set(binds))
|
|
82
|
+
extra = sorted(set(binds) - want)
|
|
83
|
+
sys.exit(f"{name}: " + "; ".join(
|
|
84
|
+
([f"bind {', '.join(gap)} not given"] if gap else [])
|
|
85
|
+
+ ([f"{', '.join(extra)} is not a bind of this command"] if extra else [])))
|
|
86
|
+
return _fill(doc["command"], binds)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def run(conn, name, **binds):
|
|
90
|
+
"""The answer to one command file: every document of a cursor, or the
|
|
91
|
+
reply document."""
|
|
92
|
+
command = command_of(name, **binds)
|
|
93
|
+
cursor = next(iter(command)) in CURSOR_COMMANDS
|
|
94
|
+
return conn.run(name, command, cursor, _load(name).get("database") == "admin")
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def like_regex(pattern):
|
|
98
|
+
"""A LIKE-style pattern (% any run, _ one character) as an anchored,
|
|
99
|
+
case-sensitive regular expression — MongoDB names are case-sensitive."""
|
|
100
|
+
return "^" + "".join(".*" if c == "%" else "." if c == "_" else re.escape(c)
|
|
101
|
+
for c in str(pattern)) + "$"
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def ejson(value, indent=2):
|
|
105
|
+
"""value as relaxed Extended JSON: ObjectId, dates and decimals keep
|
|
106
|
+
their type ({"$oid": ...}), plain values read as plain JSON."""
|
|
107
|
+
try:
|
|
108
|
+
from bson import json_util
|
|
109
|
+
except ImportError: # the suite runs without the driver
|
|
110
|
+
return json.dumps(value, indent=indent, default=str, ensure_ascii=False)
|
|
111
|
+
return json_util.dumps(value, json_options=json_util.RELAXED_JSON_OPTIONS,
|
|
112
|
+
indent=indent, ensure_ascii=False)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def to_bson(value):
|
|
116
|
+
"""A value parsed from a statement as plain JSON, with its Extended JSON
|
|
117
|
+
forms ({"$oid": ...}, {"$date": ...}) turned into BSON types, as the
|
|
118
|
+
server must receive them."""
|
|
119
|
+
try:
|
|
120
|
+
from bson import json_util
|
|
121
|
+
except ImportError: # the suite runs without the driver
|
|
122
|
+
return value
|
|
123
|
+
return json_util.loads(json.dumps(value))
|
|
124
|
+
|
pythia_mongo/cli.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"purpose": "one collection or view by its exact name",
|
|
3
|
+
"binds": {
|
|
4
|
+
"n": "the name, as written"
|
|
5
|
+
},
|
|
6
|
+
"returns": "listCollections cursor: at most one document (name, type, options)",
|
|
7
|
+
"command": {
|
|
8
|
+
"listCollections": 1,
|
|
9
|
+
"filter": {
|
|
10
|
+
"name": {
|
|
11
|
+
"$bind": "n"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"nameOnly": false
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"purpose": "every collection and view whose name matches a pattern, system collections left out",
|
|
3
|
+
"binds": {
|
|
4
|
+
"p": "the name as an anchored regular expression (catalog.like_regex)"
|
|
5
|
+
},
|
|
6
|
+
"returns": "listCollections cursor: name, type, options (validator, validationLevel, validationAction, viewOn, pipeline, ...)",
|
|
7
|
+
"command": {
|
|
8
|
+
"listCollections": 1,
|
|
9
|
+
"filter": {
|
|
10
|
+
"$and": [
|
|
11
|
+
{
|
|
12
|
+
"name": {
|
|
13
|
+
"$regex": {
|
|
14
|
+
"$bind": "p"
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"name": {
|
|
20
|
+
"$not": {
|
|
21
|
+
"$regex": "^system\\."
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
]
|
|
26
|
+
},
|
|
27
|
+
"nameOnly": false
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"purpose": "the keys a unique index would meet more than once, for its prediction",
|
|
3
|
+
"binds": {
|
|
4
|
+
"n": "the collection",
|
|
5
|
+
"f": "the documents the index holds (partialFilterExpression, or the sparse rule)",
|
|
6
|
+
"g": "the index key as a group key: {field: \"$field\"}"
|
|
7
|
+
},
|
|
8
|
+
"returns": "aggregate cursor: up to 3 documents {_id: the key, n}",
|
|
9
|
+
"command": {
|
|
10
|
+
"aggregate": {
|
|
11
|
+
"$bind": "n"
|
|
12
|
+
},
|
|
13
|
+
"pipeline": [
|
|
14
|
+
{
|
|
15
|
+
"$match": {
|
|
16
|
+
"$bind": "f"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"$group": {
|
|
21
|
+
"_id": {
|
|
22
|
+
"$bind": "g"
|
|
23
|
+
},
|
|
24
|
+
"n": {
|
|
25
|
+
"$sum": 1
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"$match": {
|
|
31
|
+
"n": {
|
|
32
|
+
"$gt": 1
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"$limit": 3
|
|
38
|
+
}
|
|
39
|
+
],
|
|
40
|
+
"cursor": {},
|
|
41
|
+
"maxTimeMS": 30000
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"purpose": "how many documents break a collection's validator as it stands, and the first of them",
|
|
3
|
+
"binds": {
|
|
4
|
+
"n": "the collection",
|
|
5
|
+
"v": "its validator, as listCollections gives it"
|
|
6
|
+
},
|
|
7
|
+
"returns": "aggregate cursor: one document {n: [{n}], first: [{_id}]}",
|
|
8
|
+
"command": {
|
|
9
|
+
"aggregate": {
|
|
10
|
+
"$bind": "n"
|
|
11
|
+
},
|
|
12
|
+
"pipeline": [
|
|
13
|
+
{
|
|
14
|
+
"$match": {
|
|
15
|
+
"$nor": [
|
|
16
|
+
{
|
|
17
|
+
"$bind": "v"
|
|
18
|
+
}
|
|
19
|
+
]
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"$facet": {
|
|
24
|
+
"n": [
|
|
25
|
+
{
|
|
26
|
+
"$count": "n"
|
|
27
|
+
}
|
|
28
|
+
],
|
|
29
|
+
"first": [
|
|
30
|
+
{
|
|
31
|
+
"$limit": 3
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"$project": {
|
|
35
|
+
"_id": 1
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
]
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
],
|
|
42
|
+
"cursor": {},
|
|
43
|
+
"maxTimeMS": 30000
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"purpose": "a view's pipeline run over its source, for its prediction: once over no documents (does MongoDB accept it?) and once over the first documents (does it run?)",
|
|
3
|
+
"binds": {
|
|
4
|
+
"n": "the collection or view the pipeline reads",
|
|
5
|
+
"p": "the pipeline, ending in $count; classify refuses $out, $merge and JavaScript in any statement it comes from"
|
|
6
|
+
},
|
|
7
|
+
"returns": "aggregate cursor: one document {n}, or none",
|
|
8
|
+
"command": {
|
|
9
|
+
"aggregate": {
|
|
10
|
+
"$bind": "n"
|
|
11
|
+
},
|
|
12
|
+
"pipeline": {
|
|
13
|
+
"$bind": "p"
|
|
14
|
+
},
|
|
15
|
+
"cursor": {},
|
|
16
|
+
"maxTimeMS": 30000
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"purpose": "the first documents an UPDATE or DELETE filter matches, for the preview",
|
|
3
|
+
"binds": {
|
|
4
|
+
"n": "the collection",
|
|
5
|
+
"f": "the statement's own filter",
|
|
6
|
+
"k": "one more than are shown, so a cut is announced"
|
|
7
|
+
},
|
|
8
|
+
"returns": "aggregate cursor: the documents",
|
|
9
|
+
"command": {
|
|
10
|
+
"aggregate": {
|
|
11
|
+
"$bind": "n"
|
|
12
|
+
},
|
|
13
|
+
"pipeline": [
|
|
14
|
+
{
|
|
15
|
+
"$match": {
|
|
16
|
+
"$bind": "f"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"$limit": {
|
|
21
|
+
"$bind": "k"
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
],
|
|
25
|
+
"cursor": {},
|
|
26
|
+
"maxTimeMS": 30000
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"purpose": "how many documents an UPDATE or DELETE filter matches, and an order-independent fingerprint of them (any document joining, leaving or changing moves it)",
|
|
3
|
+
"binds": {
|
|
4
|
+
"n": "the collection",
|
|
5
|
+
"f": "the statement's own filter"
|
|
6
|
+
},
|
|
7
|
+
"returns": "aggregate cursor: one document {n, h}, or none when nothing matches",
|
|
8
|
+
"command": {
|
|
9
|
+
"aggregate": {
|
|
10
|
+
"$bind": "n"
|
|
11
|
+
},
|
|
12
|
+
"pipeline": [
|
|
13
|
+
{
|
|
14
|
+
"$match": {
|
|
15
|
+
"$bind": "f"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"$group": {
|
|
20
|
+
"_id": null,
|
|
21
|
+
"n": {
|
|
22
|
+
"$sum": 1
|
|
23
|
+
},
|
|
24
|
+
"h": {
|
|
25
|
+
"$sum": {
|
|
26
|
+
"$toDecimal": {
|
|
27
|
+
"$toHashedIndexKey": "$$ROOT"
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
],
|
|
34
|
+
"cursor": {},
|
|
35
|
+
"maxTimeMS": 30000
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"purpose": "a random sample of documents, for `cols` to read field names and types from",
|
|
3
|
+
"binds": {
|
|
4
|
+
"n": "the collection or view",
|
|
5
|
+
"k": "how many documents at most"
|
|
6
|
+
},
|
|
7
|
+
"returns": "aggregate cursor: the sampled documents",
|
|
8
|
+
"command": {
|
|
9
|
+
"aggregate": {
|
|
10
|
+
"$bind": "n"
|
|
11
|
+
},
|
|
12
|
+
"pipeline": [
|
|
13
|
+
{
|
|
14
|
+
"$sample": {
|
|
15
|
+
"size": {
|
|
16
|
+
"$bind": "k"
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
],
|
|
21
|
+
"cursor": {}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"purpose": "whether a view's pipeline still runs, over its first documents",
|
|
3
|
+
"binds": {
|
|
4
|
+
"n": "the view",
|
|
5
|
+
"k": "how many documents to run it on"
|
|
6
|
+
},
|
|
7
|
+
"returns": "aggregate cursor: one document {n} (or none for an empty view)",
|
|
8
|
+
"command": {
|
|
9
|
+
"aggregate": {
|
|
10
|
+
"$bind": "n"
|
|
11
|
+
},
|
|
12
|
+
"pipeline": [
|
|
13
|
+
{
|
|
14
|
+
"$limit": {
|
|
15
|
+
"$bind": "k"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"$count": "n"
|
|
20
|
+
}
|
|
21
|
+
],
|
|
22
|
+
"cursor": {}
|
|
23
|
+
}
|
|
24
|
+
}
|