context-graph-spaces 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.
@@ -0,0 +1,189 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ share/python-wheels/
24
+ *.egg-info/
25
+ .installed.cfg
26
+ *.egg
27
+ MANIFEST
28
+
29
+ # PyInstaller
30
+ # Usually these files are written by a python script from a template
31
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
32
+ *.manifest
33
+ *.spec
34
+
35
+ # Installer logs
36
+ pip-log.txt
37
+ pip-delete-this-directory.txt
38
+
39
+ # Unit test / coverage reports
40
+ htmlcov/
41
+ .tox/
42
+ .nox/
43
+ .coverage
44
+ .coverage.*
45
+ .cache
46
+ nosetests.xml
47
+ coverage.xml
48
+ *.cover
49
+ *.py,cover
50
+ .hypothesis/
51
+ .pytest_cache/
52
+ cover/
53
+
54
+ # Translations
55
+ *.mo
56
+ *.pot
57
+
58
+ # Django stuff:
59
+ *.log
60
+ local_settings.py
61
+ db.sqlite3
62
+ db.sqlite3-journal
63
+
64
+ # Flask stuff:
65
+ instance/
66
+ .webassets-cache
67
+
68
+ # Scrapy stuff:
69
+ .scrapy
70
+
71
+ # Sphinx documentation
72
+ docs/_build/
73
+
74
+ # PyBuilder
75
+ .pybuilder/
76
+ target/
77
+
78
+ # Jupyter Notebook
79
+ .ipynb_checkpoints
80
+
81
+ # IPython
82
+ profile_default/
83
+ ipython_config.py
84
+
85
+ # pyenv
86
+ # For a library or package, you might want to ignore these files since the code is
87
+ # intended to run in multiple environments; otherwise, check them in:
88
+ # .python-version
89
+
90
+ # pipenv
91
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
93
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
94
+ # install all needed dependencies.
95
+ #Pipfile.lock
96
+
97
+ # UV
98
+ # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
99
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
100
+ # commonly ignored for libraries.
101
+ #uv.lock
102
+
103
+ # poetry
104
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
105
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
106
+ # commonly ignored for libraries.
107
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
108
+ #poetry.lock
109
+
110
+ # pdm
111
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
112
+ #pdm.lock
113
+ # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
114
+ # in version control.
115
+ # https://pdm.fming.dev/latest/usage/project/#working-with-version-control
116
+ .pdm.toml
117
+ .pdm-python
118
+ .pdm-build/
119
+
120
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
121
+ __pypackages__/
122
+
123
+ # Celery stuff
124
+ celerybeat-schedule
125
+ celerybeat.pid
126
+
127
+ # SageMath parsed files
128
+ *.sage.py
129
+
130
+ # Environments
131
+ .env
132
+ .venv
133
+ env/
134
+ venv/
135
+ ENV/
136
+ env.bak/
137
+ venv.bak/
138
+
139
+ # Spyder project settings
140
+ .spyderproject
141
+ .spyproject
142
+
143
+ # Rope project settings
144
+ .ropeproject
145
+
146
+ # mkdocs documentation
147
+ /site
148
+
149
+ # mypy
150
+ .mypy_cache/
151
+ .dmypy.json
152
+ dmypy.json
153
+
154
+ # Pyre type checker
155
+ .pyre/
156
+
157
+ # pytype static type analyzer
158
+ .pytype/
159
+
160
+ # Cython debug symbols
161
+ cython_debug/
162
+
163
+ # PyCharm
164
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
165
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
166
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
167
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
168
+ #.idea/
169
+
170
+ # Ruff stuff:
171
+ .ruff_cache/
172
+
173
+ # PyPI configuration file
174
+ .pypirc
175
+
176
+
177
+ # VS Code
178
+ .vscode/
179
+
180
+
181
+ # Local agent hook configuration
182
+ /.codex/
183
+ /.claude/settings.local.json
184
+
185
+ # Project specific files
186
+ /enterprise-context/sic-agent/sic-scrapper/output/*
187
+
188
+ # deepeval writes run caches and telemetry here; build artifacts, not source.
189
+ .deepeval/
@@ -0,0 +1,37 @@
1
+ # Context Graph Spaces
2
+
3
+ The Enterprise edition of Context Graph: memory for many users on one shared,
4
+ licensed Memgraph, where Memgraph's own access control enforces the isolation
5
+ (map #501).
6
+
7
+ ## Language
8
+
9
+ **Personal Space**:
10
+ A member's own database, `u_<login>`, and their main database. Hooks write all of the member's activity here. Only the member and nobody else can reach it.
11
+ _Avoid_: tenant, user database
12
+
13
+ **Shared Space**:
14
+ A database `space_<name>` that a set of members can read. Members may create and change only the labels and edge types the **Spaces Spec** allows; v1 allows curated memory only. A team or the whole org is just a Shared Space with different members.
15
+ _Avoid_: team space (a team is one use), workspace
16
+
17
+ **Spaces Spec**:
18
+ The admin's TOML file listing members and Shared Spaces. It is the single source of desired state. `context-graph-spaces plan` diffs Memgraph against it; `apply` makes Memgraph match.
19
+ _Avoid_: config (that's the member's own Context Graph config file)
20
+
21
+ **Space Membership**:
22
+ A `(:SpaceMembership {space, database})` node in a member's Personal Space, written by `apply`. It is how recall learns which Shared Spaces to read, because an unprivileged login can't list its own grants. Memgraph's grants remain the authority; a stale node only costs a permission error.
23
+
24
+ **Service Account**:
25
+ `cg_service`. Holds the worker role on every Shared Space and writes what members can't there, such as Chunks and entities from reconciliation.
26
+
27
+ **Owned Object**:
28
+ A database, role, user or tenant profile with a `u_`, `space_` or `cg_` prefix, or a user whose `u_<login>` database exists. The provisioner changes only owned objects.
29
+
30
+ ## Relationships
31
+
32
+ - **Spaces Spec -> Memgraph**: `apply` creates databases, versioned roles (named by a hash of their grants), users, role assignments scoped to one database, tenant profiles and Space Memberships. Removing a member or a space revokes access; `--prune` also drops users and databases.
33
+ - **Context Graph config -> EnterpriseSpaces**: with `edition = "enterprise"`, memgraph-context-graph's `SpaceResolver` is this package's resolver, found through the `memgraph_context_graph.editions` entry point.
34
+
35
+ ## Flagged ambiguities
36
+
37
+ - "Role": in this package, a Memgraph auth role (`space_payments_member_3fa2c1d0`). Not a message speaker role (`actions-graph`'s `MessageRole`).
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Memgraph
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,106 @@
1
+ Metadata-Version: 2.5
2
+ Name: context-graph-spaces
3
+ Version: 0.1.0
4
+ Summary: The Enterprise edition of Context Graph: personal and shared memory spaces for many users on one Memgraph
5
+ License: MIT
6
+ License-File: LICENSE
7
+ Classifier: Programming Language :: Python :: 3.10
8
+ Classifier: Programming Language :: Python :: 3.11
9
+ Classifier: Programming Language :: Python :: 3.12
10
+ Classifier: Programming Language :: Python :: 3.13
11
+ Requires-Python: >=3.10
12
+ Requires-Dist: memgraph-context-graph>=0.5.0
13
+ Requires-Dist: memgraph-toolbox
14
+ Requires-Dist: neo4j>=5.0
15
+ Requires-Dist: sessions-graph[reconciliation]>=0.6.0
16
+ Requires-Dist: tomli>=2.0
17
+ Provides-Extra: test
18
+ Requires-Dist: pytest>=9.0.3; extra == 'test'
19
+ Requires-Dist: sessions-graph[memgraph-context-graph,reconciliation]>=0.8.0; extra == 'test'
20
+ Description-Content-Type: text/markdown
21
+
22
+ # context-graph-spaces
23
+
24
+ The Enterprise edition of [Context Graph](../README.md): memory for many users on
25
+ one shared, licensed Memgraph. Each user gets a **personal space**, and teams get
26
+ **shared spaces**. Memgraph Enterprise enforces the isolation through
27
+ multi-tenancy, role-based access control and label- and property-based access
28
+ control. Our code doesn't filter.
29
+
30
+ The Community edition (one user, one database) doesn't need this package and
31
+ doesn't change when it's installed. A user's config file selects Enterprise
32
+ explicitly:
33
+
34
+ ```toml
35
+ edition = "enterprise"
36
+
37
+ [memgraph]
38
+ url = "bolt://memgraph.corp:7687"
39
+ user = "ante" # your Memgraph login is your identity
40
+ password = "..."
41
+ ```
42
+
43
+ ## For admins: provisioning
44
+
45
+ Spaces are declared in a spec file and applied idempotently:
46
+
47
+ ```toml
48
+ members = ["ante", "bob"]
49
+ personal_memory_limit_mb = 2048
50
+
51
+ [spaces.payments]
52
+ members = ["ante", "bob"]
53
+ memory_limit_mb = 4096
54
+ # labels = [...] # what members may write; defaults to curated memory
55
+ # edge_types = [...]
56
+ ```
57
+
58
+ ```bash
59
+ export MEMGRAPH_URL=bolt://memgraph.corp:7687 MEMGRAPH_USER=<admin> MEMGRAPH_PASSWORD=...
60
+ context-graph-spaces plan spaces.toml # what would change
61
+ context-graph-spaces apply spaces.toml # make it so; prints one-time setup lines for new logins
62
+ ```
63
+
64
+ What `apply` maintains:
65
+
66
+ | Object | Name | Who |
67
+ |---|---|---|
68
+ | Personal database | `u_<login>` | its owner (main database), plus nobody else |
69
+ | Shared database | `space_<name>` | members: read everything, write only the spec's labels |
70
+ | Roles | `cg_u_<login>_<hash>`, `space_<name>_member_<hash>`, `cg_worker_<hash>` | named after what they grant |
71
+ | Service login | `cg_service` | writes Chunks and entities in shared spaces |
72
+ | Tenant profiles | `cg_<mb>mb` | memory caps from the spec |
73
+
74
+ Removing someone from the spec revokes their access. Removing a space revokes
75
+ access to it. Users and databases are only dropped with `--prune`. Only objects
76
+ with the `u_`, `space_` and `cg_` prefixes are ever touched.
77
+
78
+ ## The shared-space worker
79
+
80
+ Members can write only curated memory into a shared space. The derived graph
81
+ there (chunks, entities, relations, and the embeddings recall uses) is written by
82
+ one service login, `cg_service`. Run the worker next to `apply`, with the
83
+ password `apply` printed once:
84
+
85
+ ```bash
86
+ export CG_SERVICE_PASSWORD=...
87
+ context-graph-spaces reconcile spaces.toml --watch # one pass every 30s
88
+ ```
89
+
90
+ Each pass, for each shared space, it does two things:
91
+
92
+ 1. It removes what deleted files left behind. A member's delete can't touch chunks.
93
+ 2. It reconciles changed memory files with GLiNER2 under the space's ontology.
94
+ No LLM is involved.
95
+
96
+ Personal spaces keep reconciling locally, as in Community.
97
+
98
+ ## Tests
99
+
100
+ The tests run end-to-end against a licensed Memgraph. The licence is read from
101
+ `MEMGRAPH_ENTERPRISE_LICENSE` and `MEMGRAPH_ORGANIZATION_NAME` (the repo's `.env`
102
+ locally, secrets in CI):
103
+
104
+ ```bash
105
+ ./scripts/dev-memgraph.sh test-enterprise
106
+ ```
@@ -0,0 +1,85 @@
1
+ # context-graph-spaces
2
+
3
+ The Enterprise edition of [Context Graph](../README.md): memory for many users on
4
+ one shared, licensed Memgraph. Each user gets a **personal space**, and teams get
5
+ **shared spaces**. Memgraph Enterprise enforces the isolation through
6
+ multi-tenancy, role-based access control and label- and property-based access
7
+ control. Our code doesn't filter.
8
+
9
+ The Community edition (one user, one database) doesn't need this package and
10
+ doesn't change when it's installed. A user's config file selects Enterprise
11
+ explicitly:
12
+
13
+ ```toml
14
+ edition = "enterprise"
15
+
16
+ [memgraph]
17
+ url = "bolt://memgraph.corp:7687"
18
+ user = "ante" # your Memgraph login is your identity
19
+ password = "..."
20
+ ```
21
+
22
+ ## For admins: provisioning
23
+
24
+ Spaces are declared in a spec file and applied idempotently:
25
+
26
+ ```toml
27
+ members = ["ante", "bob"]
28
+ personal_memory_limit_mb = 2048
29
+
30
+ [spaces.payments]
31
+ members = ["ante", "bob"]
32
+ memory_limit_mb = 4096
33
+ # labels = [...] # what members may write; defaults to curated memory
34
+ # edge_types = [...]
35
+ ```
36
+
37
+ ```bash
38
+ export MEMGRAPH_URL=bolt://memgraph.corp:7687 MEMGRAPH_USER=<admin> MEMGRAPH_PASSWORD=...
39
+ context-graph-spaces plan spaces.toml # what would change
40
+ context-graph-spaces apply spaces.toml # make it so; prints one-time setup lines for new logins
41
+ ```
42
+
43
+ What `apply` maintains:
44
+
45
+ | Object | Name | Who |
46
+ |---|---|---|
47
+ | Personal database | `u_<login>` | its owner (main database), plus nobody else |
48
+ | Shared database | `space_<name>` | members: read everything, write only the spec's labels |
49
+ | Roles | `cg_u_<login>_<hash>`, `space_<name>_member_<hash>`, `cg_worker_<hash>` | named after what they grant |
50
+ | Service login | `cg_service` | writes Chunks and entities in shared spaces |
51
+ | Tenant profiles | `cg_<mb>mb` | memory caps from the spec |
52
+
53
+ Removing someone from the spec revokes their access. Removing a space revokes
54
+ access to it. Users and databases are only dropped with `--prune`. Only objects
55
+ with the `u_`, `space_` and `cg_` prefixes are ever touched.
56
+
57
+ ## The shared-space worker
58
+
59
+ Members can write only curated memory into a shared space. The derived graph
60
+ there (chunks, entities, relations, and the embeddings recall uses) is written by
61
+ one service login, `cg_service`. Run the worker next to `apply`, with the
62
+ password `apply` printed once:
63
+
64
+ ```bash
65
+ export CG_SERVICE_PASSWORD=...
66
+ context-graph-spaces reconcile spaces.toml --watch # one pass every 30s
67
+ ```
68
+
69
+ Each pass, for each shared space, it does two things:
70
+
71
+ 1. It removes what deleted files left behind. A member's delete can't touch chunks.
72
+ 2. It reconciles changed memory files with GLiNER2 under the space's ontology.
73
+ No LLM is involved.
74
+
75
+ Personal spaces keep reconciling locally, as in Community.
76
+
77
+ ## Tests
78
+
79
+ The tests run end-to-end against a licensed Memgraph. The licence is read from
80
+ `MEMGRAPH_ENTERPRISE_LICENSE` and `MEMGRAPH_ORGANIZATION_NAME` (the repo's `.env`
81
+ locally, secrets in CI):
82
+
83
+ ```bash
84
+ ./scripts/dev-memgraph.sh test-enterprise
85
+ ```
@@ -0,0 +1,44 @@
1
+ [project]
2
+ name = "context-graph-spaces"
3
+ version = "0.1.0"
4
+ description = "The Enterprise edition of Context Graph: personal and shared memory spaces for many users on one Memgraph"
5
+ readme = "README.md"
6
+ license = { text = "MIT" }
7
+ requires-python = ">=3.10"
8
+ classifiers = [
9
+ "Programming Language :: Python :: 3.10",
10
+ "Programming Language :: Python :: 3.11",
11
+ "Programming Language :: Python :: 3.12",
12
+ "Programming Language :: Python :: 3.13",
13
+ ]
14
+ dependencies = [
15
+ "memgraph-context-graph>=0.5.0",
16
+ "memgraph-toolbox",
17
+ # Shared spaces hold sessions-graph's memory files: apply sets up its schema there, and the
18
+ # worker reconciles them (the reconciliation extra brings unstructured2graph).
19
+ "sessions-graph[reconciliation]>=0.6.0",
20
+ "neo4j>=5.0",
21
+ # tomllib on 3.11+; tomli for 3.10, which ty checks against.
22
+ "tomli>=2.0",
23
+ ]
24
+
25
+ [project.optional-dependencies]
26
+ test = [
27
+ "pytest>=9.0.3",
28
+ "sessions-graph[memgraph-context-graph,reconciliation]>=0.8.0",
29
+ ]
30
+
31
+ [project.scripts]
32
+ context-graph-spaces = "context_graph_spaces.cli:main"
33
+
34
+ [project.entry-points."memgraph_context_graph.editions"]
35
+ enterprise = "context_graph_spaces.resolver:EnterpriseSpaces"
36
+
37
+ [tool.uv.sources]
38
+ memgraph-context-graph = { workspace = true }
39
+ memgraph-toolbox = { workspace = true }
40
+ sessions-graph = { workspace = true }
41
+
42
+ [build-system]
43
+ requires = ["hatchling"]
44
+ build-backend = "hatchling.build"
@@ -0,0 +1,13 @@
1
+ """Context Graph's Enterprise edition: personal and shared memory spaces for many users on one Memgraph.
2
+
3
+ Selected by ``edition = "enterprise"`` in the Context Graph config file, which
4
+ makes :class:`EnterpriseSpaces` the space resolver. The admin side provisions
5
+ spaces from a spec (:mod:`.spec`, :mod:`.provision`, ``context-graph-spaces
6
+ plan|apply``).
7
+ """
8
+
9
+ from .provision import Applied, Plan, apply, observed_state, plan
10
+ from .resolver import EnterpriseSpaces
11
+ from .spec import Spec, SpecError, load_spec
12
+
13
+ __all__ = ["Applied", "EnterpriseSpaces", "Plan", "Spec", "SpecError", "apply", "load_spec", "observed_state", "plan"]
@@ -0,0 +1,123 @@
1
+ """``context-graph-spaces``: the admin's commands for provisioning spaces from a spec.
2
+
3
+ The admin connection comes from flags, falling back to ``MEMGRAPH_URL`` /
4
+ ``MEMGRAPH_USER`` / ``MEMGRAPH_PASSWORD``. This is an interactive admin tool,
5
+ not a hook, so the config-file-only rule for hooks doesn't apply; the admin's
6
+ login is never written anywhere.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import argparse
12
+ import os
13
+ import sys
14
+ from typing import TYPE_CHECKING, Any, cast
15
+
16
+ from .provision import SERVICE_ACCOUNT, Plan, Query, Step, apply, observed_state, plan
17
+ from .spec import SpecError, load_spec
18
+
19
+ if TYPE_CHECKING:
20
+ from collections.abc import Sequence
21
+
22
+ from .spec import Spec
23
+
24
+ #: Where the reconcile worker finds the service login's password.
25
+ SERVICE_PASSWORD_ENV = "CG_SERVICE_PASSWORD"
26
+
27
+
28
+ def main(argv: Sequence[str] | None = None) -> int:
29
+ """Run ``plan`` or ``apply``; returns the process exit code."""
30
+ parser = argparse.ArgumentParser(prog="context-graph-spaces", description=__doc__)
31
+ parser.add_argument("--url", default=os.environ.get("MEMGRAPH_URL", "bolt://localhost:7687"))
32
+ parser.add_argument("--user", default=os.environ.get("MEMGRAPH_USER", ""), help="An admin login.")
33
+ parser.add_argument("--password", default=os.environ.get("MEMGRAPH_PASSWORD", ""))
34
+ commands = parser.add_subparsers(dest="command", required=True)
35
+ for name, text in (("plan", "Show what apply would change."), ("apply", "Make Memgraph match the spec.")):
36
+ command = commands.add_parser(name, help=text)
37
+ command.add_argument("spec", help="The spaces spec (TOML).")
38
+ command.add_argument("--prune", action="store_true", help="Also drop removed users and databases.")
39
+ reconcile = commands.add_parser("reconcile", help="Reconcile shared spaces' changed memory files, as cg_service.")
40
+ reconcile.add_argument("spec", help="The spaces spec (TOML): which shared spaces to reconcile.")
41
+ reconcile.add_argument("--watch", action="store_true", help="Keep going, one pass every --interval seconds.")
42
+ reconcile.add_argument("--interval", type=float, default=30.0)
43
+ args = parser.parse_args(argv)
44
+
45
+ try:
46
+ spec = load_spec(args.spec)
47
+ except SpecError as exc:
48
+ print(exc, file=sys.stderr)
49
+ return 2
50
+ if args.command == "reconcile":
51
+ return _reconcile(spec, args.url, watch=args.watch, interval=args.interval)
52
+ query, close = admin_query(args.url, args.user, args.password)
53
+ try:
54
+ if args.command == "plan":
55
+ _print_plan(plan(spec, observed_state(query, spec.members), prune=args.prune))
56
+ return 0
57
+ applied = apply(spec, query, prune=args.prune)
58
+ finally:
59
+ close()
60
+ _print_steps(applied.steps, "Applied" if applied.steps else "Nothing to change; Memgraph matches the spec.")
61
+ _print_kept(applied.kept)
62
+ for login, password in sorted(applied.passwords.items()):
63
+ if login == SERVICE_ACCOUNT:
64
+ print(f"\nNew service login {login!r} for the shared-space reconcile worker; it isn't stored anywhere.")
65
+ print(f" Give the worker: {SERVICE_PASSWORD_ENV}='{password}'")
66
+ continue
67
+ print(f"\nNew login {login!r}. Hand this to them once; it isn't stored anywhere:")
68
+ print(f" memgraph-context-graph join {args.url} {login} --password='{password}'")
69
+ return 0
70
+
71
+
72
+ def _reconcile(spec: Spec, url: str, *, watch: bool, interval: float) -> int:
73
+ """Run the worker once, or every *interval* seconds with *watch*; exit 1 if the last pass had an error."""
74
+ import time
75
+
76
+ from .worker import reconcile_spaces
77
+
78
+ password = os.environ.get(SERVICE_PASSWORD_ENV)
79
+ if not password:
80
+ print(f"Set {SERVICE_PASSWORD_ENV} to {SERVICE_ACCOUNT}'s password (apply printed it once).", file=sys.stderr)
81
+ return 2
82
+ while True:
83
+ reports = reconcile_spaces(spec, url=url, password=password)
84
+ for report in reports:
85
+ if report.error:
86
+ print(f"{report.space}: not reconciled: {report.error}", file=sys.stderr)
87
+ continue
88
+ failed = "".join(f"\n failed {path}: {error}" for path, error in report.failed)
89
+ print(f"{report.space}: {len(report.completed)} reconciled, {report.pruned} leftovers removed{failed}")
90
+ if not watch:
91
+ return 1 if any(report.error or report.failed for report in reports) else 0
92
+ time.sleep(interval)
93
+
94
+
95
+ def admin_query(url: str, user: str, password: str) -> tuple[Query, Any]:
96
+ """A :data:`~.provision.Query` over one admin driver, and the function that closes it."""
97
+ import neo4j
98
+
99
+ driver = neo4j.GraphDatabase.driver(url, auth=(user, password))
100
+
101
+ def query(statement: str, database: str | None, params: dict[str, Any] | None) -> list[dict[str, Any]]:
102
+ # Auth and multi-database DDL can't run in an explicit transaction, so each statement gets its own session.
103
+ with driver.session(database=database) as session:
104
+ # The driver's stubs want a LiteralString; admin DDL is built from names spec._NAME has validated.
105
+ return [record.data() for record in session.run(cast("Any", statement), params or {})]
106
+
107
+ return query, driver.close
108
+
109
+
110
+ def _print_plan(the_plan: Plan) -> None:
111
+ _print_steps(the_plan.steps, "Plan:" if the_plan.steps else "Nothing to change; Memgraph matches the spec.")
112
+ _print_kept(the_plan.kept)
113
+
114
+
115
+ def _print_steps(steps: list[Step], heading: str) -> None:
116
+ print(heading)
117
+ for step in steps:
118
+ print(f" + {step.summary}")
119
+
120
+
121
+ def _print_kept(kept: list[str]) -> None:
122
+ if kept:
123
+ print(f"Kept (pass --prune to drop): {', '.join(kept)}")