stashapp-client 0.1.3__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.
Files changed (39) hide show
  1. stashapp_client-0.1.3/LICENSE +21 -0
  2. stashapp_client-0.1.3/PKG-INFO +205 -0
  3. stashapp_client-0.1.3/README.md +184 -0
  4. stashapp_client-0.1.3/pyproject.toml +49 -0
  5. stashapp_client-0.1.3/setup.cfg +4 -0
  6. stashapp_client-0.1.3/src/stashapp_client/__init__.py +74 -0
  7. stashapp_client-0.1.3/src/stashapp_client/cli.py +93 -0
  8. stashapp_client-0.1.3/src/stashapp_client/client.py +126 -0
  9. stashapp_client-0.1.3/src/stashapp_client/codegen.py +176 -0
  10. stashapp_client-0.1.3/src/stashapp_client/criteria.py +73 -0
  11. stashapp_client-0.1.3/src/stashapp_client/errors.py +29 -0
  12. stashapp_client-0.1.3/src/stashapp_client/filters.py +64 -0
  13. stashapp_client-0.1.3/src/stashapp_client/fragments.py +56 -0
  14. stashapp_client-0.1.3/src/stashapp_client/generated/__init__.py +1 -0
  15. stashapp_client-0.1.3/src/stashapp_client/generated/fragments.py +111 -0
  16. stashapp_client-0.1.3/src/stashapp_client/generated/inputs.py +13 -0
  17. stashapp_client-0.1.3/src/stashapp_client/generated/operations_registry.json +10012 -0
  18. stashapp_client-0.1.3/src/stashapp_client/generated/schema.json +43838 -0
  19. stashapp_client-0.1.3/src/stashapp_client/input_validation.py +83 -0
  20. stashapp_client-0.1.3/src/stashapp_client/introspection.py +104 -0
  21. stashapp_client-0.1.3/src/stashapp_client/mutation.py +129 -0
  22. stashapp_client-0.1.3/src/stashapp_client/pagination.py +57 -0
  23. stashapp_client-0.1.3/src/stashapp_client/py.typed +0 -0
  24. stashapp_client-0.1.3/src/stashapp_client/registry.py +125 -0
  25. stashapp_client-0.1.3/src/stashapp_client/response.py +64 -0
  26. stashapp_client-0.1.3/src/stashapp_client/runtime_bind.py +201 -0
  27. stashapp_client-0.1.3/src/stashapp_client/schema_diff.py +134 -0
  28. stashapp_client-0.1.3/src/stashapp_client.egg-info/PKG-INFO +205 -0
  29. stashapp_client-0.1.3/src/stashapp_client.egg-info/SOURCES.txt +37 -0
  30. stashapp_client-0.1.3/src/stashapp_client.egg-info/dependency_links.txt +1 -0
  31. stashapp_client-0.1.3/src/stashapp_client.egg-info/entry_points.txt +2 -0
  32. stashapp_client-0.1.3/src/stashapp_client.egg-info/requires.txt +8 -0
  33. stashapp_client-0.1.3/src/stashapp_client.egg-info/top_level.txt +1 -0
  34. stashapp_client-0.1.3/tests/test_client.py +178 -0
  35. stashapp_client-0.1.3/tests/test_codegen.py +321 -0
  36. stashapp_client-0.1.3/tests/test_filters.py +35 -0
  37. stashapp_client-0.1.3/tests/test_mutation.py +49 -0
  38. stashapp_client-0.1.3/tests/test_registry.py +333 -0
  39. stashapp_client-0.1.3/tests/test_sdl_generation.py +28 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Written2001
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,205 @@
1
+ Metadata-Version: 2.4
2
+ Name: stashapp-client
3
+ Version: 0.1.3
4
+ Summary: Python client for the Stash GraphQL API
5
+ Author-email: Written2001 <knasdkn@protonmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/Written2001/stash_client
8
+ Project-URL: Repository, https://github.com/Written2001/stash_client
9
+ Project-URL: Issues, https://github.com/Written2001/stash_client/issues
10
+ Requires-Python: >=3.10
11
+ Description-Content-Type: text/markdown
12
+ License-File: LICENSE
13
+ Requires-Dist: graphql-core>=3.2
14
+ Requires-Dist: pandas>=2.0
15
+ Requires-Dist: requests>=2.31
16
+ Provides-Extra: dev
17
+ Requires-Dist: pytest>=8.0; extra == "dev"
18
+ Requires-Dist: ruff>=0.6; extra == "dev"
19
+ Requires-Dist: mypy>=1.10; extra == "dev"
20
+ Dynamic: license-file
21
+
22
+ # stashapp-client
23
+
24
+ An SDL-first Python client for the [Stash](https://github.com/stashapp/stash) GraphQL API. The package generates an operation registry from the Stash schema and binds query and mutation methods to `StashClient` at runtime.
25
+
26
+ ## Installation
27
+
28
+ Install the latest published release from PyPI:
29
+
30
+ ```bash
31
+ python -m pip install stashapp-client
32
+ ```
33
+
34
+ For development from a checkout of this repository:
35
+
36
+ ```bash
37
+ python -m pip install -e .
38
+ ```
39
+
40
+ For development tools:
41
+
42
+ ```bash
43
+ python -m pip install -e '.[dev]'
44
+ ```
45
+
46
+ Releases follow [Semantic Versioning](https://semver.org/). See
47
+ [`CHANGELOG.md`](CHANGELOG.md) for release history.
48
+
49
+ ## Connect
50
+
51
+ Create a credentials file containing the GraphQL URL and API key on separate lines:
52
+
53
+ ```text
54
+ https://stash.example/graphql
55
+ YOUR_API_KEY
56
+ ```
57
+
58
+ Then create a client:
59
+
60
+ ```python
61
+ from stashapp_client import StashClient
62
+
63
+ client = StashClient.from_credentials_file(".stash_credentials")
64
+ print(client.has_connection())
65
+ ```
66
+
67
+ Credentials can also come directly from arguments or environment variables:
68
+
69
+ ```python
70
+ client = StashClient(
71
+ url="https://stash.example/graphql",
72
+ api_key="YOUR_API_KEY",
73
+ )
74
+
75
+ # Reads STASH_URL and STASH_API_KEY.
76
+ client = StashClient.from_env()
77
+ ```
78
+
79
+ Always close the client when finished, or use it as a context manager:
80
+
81
+ ```python
82
+ with StashClient.from_credentials_file(".stash_credentials") as client:
83
+ print(client.findTags())
84
+ ```
85
+
86
+ ## TLS
87
+
88
+ Pass `verify=False` only for a trusted internal endpoint. A CA bundle path is preferred for self-signed certificates:
89
+
90
+ ```python
91
+ client = StashClient.from_credentials_file(
92
+ ".stash_credentials",
93
+ verify="/path/to/internal-ca.pem",
94
+ )
95
+ ```
96
+
97
+ `STASHAPI_TLS_VERIFY=false` disables verification, while a path value selects a CA bundle.
98
+
99
+ ## Use The API
100
+
101
+ Operation methods are created from the bundled registry, so queries and mutations use schema-defined names and keyword arguments:
102
+
103
+ ```python
104
+ from stashapp_client import between, find_filter, includes, scene_filter
105
+
106
+ scenes = client.findScenes(
107
+ scene_filter=scene_filter(
108
+ title=includes("vacation"),
109
+ rating100=between(80, 100),
110
+ tags=includes(182),
111
+ ),
112
+ filter=find_filter(per_page=50),
113
+ )
114
+
115
+ tags = client.findTags(
116
+ tag_filter={"favorite": True},
117
+ filter=find_filter(per_page=50),
118
+ )
119
+ ```
120
+
121
+ Filter arguments use the GraphQL operation names, such as `scene_filter`,
122
+ `gallery_filter`, and `tag_filter`. The `*_filter()` helpers cover common
123
+ criteria and reject unknown helper fields. You can also pass a raw dictionary
124
+ for any field in the generated schema:
125
+
126
+ ```python
127
+ scenes = client.findScenes(
128
+ scene_filter={
129
+ "organized": False,
130
+ "duration": {"modifier": "GREATER_THAN", "value": 600},
131
+ "performers": {"modifier": "INCLUDES", "value": 84},
132
+ }
133
+ )
134
+ ```
135
+
136
+ Mutations use the same generated operation methods and schema-defined argument names:
137
+
138
+ ```python
139
+ new_tag = client.tagCreate(input={"name": "Reviewed"})
140
+ client.tagDestroy(input={"id": "123"})
141
+ ```
142
+
143
+ Set `per_page` to `-1` to fetch all pages of a list result:
144
+
145
+ ```python
146
+ all_tags = client.findTags(filter={"per_page": -1})
147
+ ```
148
+
149
+ List-of-object results are returned as `pandas.DataFrame`; scalar results are returned as ordinary Python values. Use `field` to extract a response path and `response` to control the response shape:
150
+
151
+ ```python
152
+ titles = client.findScenes(field=["scenes", "title"])
153
+ count = client.findTags(field="count")
154
+ metadata = client.findTags(response="object")
155
+ envelope = client.findTags(response="raw")
156
+ ```
157
+
158
+ Nested DataFrame columns remain as Python lists or dictionaries. Optional helpers are available for common transformations:
159
+
160
+ ```python
161
+ from stashapp_client.response import explode_column, flatten_column
162
+
163
+ flat = flatten_column(scenes, "studio")
164
+ tags = explode_column(scenes, "tags")
165
+ ```
166
+
167
+ ## Generate Operations
168
+
169
+ The bundled registry is generated from the Stash schema. Build artifacts from a schema snapshot with:
170
+
171
+ ```bash
172
+ stashapp-client build-ops --schema ./schema.json
173
+ ```
174
+
175
+ This writes `schema.json`, `operations_registry.json`, `fragments.py`, and `inputs.py` to the current directory by default. Override any output with `--out-schema`, `--out-registry`, `--out-fragments`, or `--out-inputs`.
176
+
177
+ Regenerate from a live server:
178
+
179
+ ```bash
180
+ stashapp-client sync-schema \
181
+ --credentials-file .stash_credentials \
182
+ --out-schema ./schema.json
183
+ ```
184
+
185
+ Compare two schema snapshots before rebuilding:
186
+
187
+ ```bash
188
+ stashapp-client diff-schema \
189
+ --base ./schema.json \
190
+ --current ./candidate-schema.json
191
+ ```
192
+
193
+ `diff-schema` exits with status `1` when changes are found, which makes it suitable for CI.
194
+
195
+ ## Custom Registry
196
+
197
+ Use a different generated registry when testing or targeting another schema version:
198
+
199
+ ```python
200
+ client = StashClient(
201
+ url="https://stash.example/graphql",
202
+ api_key="YOUR_API_KEY",
203
+ registry_path="./operations_registry.json",
204
+ )
205
+ ```
@@ -0,0 +1,184 @@
1
+ # stashapp-client
2
+
3
+ An SDL-first Python client for the [Stash](https://github.com/stashapp/stash) GraphQL API. The package generates an operation registry from the Stash schema and binds query and mutation methods to `StashClient` at runtime.
4
+
5
+ ## Installation
6
+
7
+ Install the latest published release from PyPI:
8
+
9
+ ```bash
10
+ python -m pip install stashapp-client
11
+ ```
12
+
13
+ For development from a checkout of this repository:
14
+
15
+ ```bash
16
+ python -m pip install -e .
17
+ ```
18
+
19
+ For development tools:
20
+
21
+ ```bash
22
+ python -m pip install -e '.[dev]'
23
+ ```
24
+
25
+ Releases follow [Semantic Versioning](https://semver.org/). See
26
+ [`CHANGELOG.md`](CHANGELOG.md) for release history.
27
+
28
+ ## Connect
29
+
30
+ Create a credentials file containing the GraphQL URL and API key on separate lines:
31
+
32
+ ```text
33
+ https://stash.example/graphql
34
+ YOUR_API_KEY
35
+ ```
36
+
37
+ Then create a client:
38
+
39
+ ```python
40
+ from stashapp_client import StashClient
41
+
42
+ client = StashClient.from_credentials_file(".stash_credentials")
43
+ print(client.has_connection())
44
+ ```
45
+
46
+ Credentials can also come directly from arguments or environment variables:
47
+
48
+ ```python
49
+ client = StashClient(
50
+ url="https://stash.example/graphql",
51
+ api_key="YOUR_API_KEY",
52
+ )
53
+
54
+ # Reads STASH_URL and STASH_API_KEY.
55
+ client = StashClient.from_env()
56
+ ```
57
+
58
+ Always close the client when finished, or use it as a context manager:
59
+
60
+ ```python
61
+ with StashClient.from_credentials_file(".stash_credentials") as client:
62
+ print(client.findTags())
63
+ ```
64
+
65
+ ## TLS
66
+
67
+ Pass `verify=False` only for a trusted internal endpoint. A CA bundle path is preferred for self-signed certificates:
68
+
69
+ ```python
70
+ client = StashClient.from_credentials_file(
71
+ ".stash_credentials",
72
+ verify="/path/to/internal-ca.pem",
73
+ )
74
+ ```
75
+
76
+ `STASHAPI_TLS_VERIFY=false` disables verification, while a path value selects a CA bundle.
77
+
78
+ ## Use The API
79
+
80
+ Operation methods are created from the bundled registry, so queries and mutations use schema-defined names and keyword arguments:
81
+
82
+ ```python
83
+ from stashapp_client import between, find_filter, includes, scene_filter
84
+
85
+ scenes = client.findScenes(
86
+ scene_filter=scene_filter(
87
+ title=includes("vacation"),
88
+ rating100=between(80, 100),
89
+ tags=includes(182),
90
+ ),
91
+ filter=find_filter(per_page=50),
92
+ )
93
+
94
+ tags = client.findTags(
95
+ tag_filter={"favorite": True},
96
+ filter=find_filter(per_page=50),
97
+ )
98
+ ```
99
+
100
+ Filter arguments use the GraphQL operation names, such as `scene_filter`,
101
+ `gallery_filter`, and `tag_filter`. The `*_filter()` helpers cover common
102
+ criteria and reject unknown helper fields. You can also pass a raw dictionary
103
+ for any field in the generated schema:
104
+
105
+ ```python
106
+ scenes = client.findScenes(
107
+ scene_filter={
108
+ "organized": False,
109
+ "duration": {"modifier": "GREATER_THAN", "value": 600},
110
+ "performers": {"modifier": "INCLUDES", "value": 84},
111
+ }
112
+ )
113
+ ```
114
+
115
+ Mutations use the same generated operation methods and schema-defined argument names:
116
+
117
+ ```python
118
+ new_tag = client.tagCreate(input={"name": "Reviewed"})
119
+ client.tagDestroy(input={"id": "123"})
120
+ ```
121
+
122
+ Set `per_page` to `-1` to fetch all pages of a list result:
123
+
124
+ ```python
125
+ all_tags = client.findTags(filter={"per_page": -1})
126
+ ```
127
+
128
+ List-of-object results are returned as `pandas.DataFrame`; scalar results are returned as ordinary Python values. Use `field` to extract a response path and `response` to control the response shape:
129
+
130
+ ```python
131
+ titles = client.findScenes(field=["scenes", "title"])
132
+ count = client.findTags(field="count")
133
+ metadata = client.findTags(response="object")
134
+ envelope = client.findTags(response="raw")
135
+ ```
136
+
137
+ Nested DataFrame columns remain as Python lists or dictionaries. Optional helpers are available for common transformations:
138
+
139
+ ```python
140
+ from stashapp_client.response import explode_column, flatten_column
141
+
142
+ flat = flatten_column(scenes, "studio")
143
+ tags = explode_column(scenes, "tags")
144
+ ```
145
+
146
+ ## Generate Operations
147
+
148
+ The bundled registry is generated from the Stash schema. Build artifacts from a schema snapshot with:
149
+
150
+ ```bash
151
+ stashapp-client build-ops --schema ./schema.json
152
+ ```
153
+
154
+ This writes `schema.json`, `operations_registry.json`, `fragments.py`, and `inputs.py` to the current directory by default. Override any output with `--out-schema`, `--out-registry`, `--out-fragments`, or `--out-inputs`.
155
+
156
+ Regenerate from a live server:
157
+
158
+ ```bash
159
+ stashapp-client sync-schema \
160
+ --credentials-file .stash_credentials \
161
+ --out-schema ./schema.json
162
+ ```
163
+
164
+ Compare two schema snapshots before rebuilding:
165
+
166
+ ```bash
167
+ stashapp-client diff-schema \
168
+ --base ./schema.json \
169
+ --current ./candidate-schema.json
170
+ ```
171
+
172
+ `diff-schema` exits with status `1` when changes are found, which makes it suitable for CI.
173
+
174
+ ## Custom Registry
175
+
176
+ Use a different generated registry when testing or targeting another schema version:
177
+
178
+ ```python
179
+ client = StashClient(
180
+ url="https://stash.example/graphql",
181
+ api_key="YOUR_API_KEY",
182
+ registry_path="./operations_registry.json",
183
+ )
184
+ ```
@@ -0,0 +1,49 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "stashapp-client"
7
+ version = "0.1.3"
8
+ description = "Python client for the Stash GraphQL API"
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ license-files = ["LICENSE"]
12
+ authors = [{name = "Written2001", email = "knasdkn@protonmail.com"}]
13
+ requires-python = ">=3.10"
14
+ dependencies = [
15
+ "graphql-core>=3.2",
16
+ "pandas>=2.0",
17
+ "requests>=2.31",
18
+ ]
19
+
20
+ [project.optional-dependencies]
21
+ dev = [
22
+ "pytest>=8.0",
23
+ "ruff>=0.6",
24
+ "mypy>=1.10",
25
+ ]
26
+
27
+ [project.scripts]
28
+ stashapp-client = "stashapp_client.cli:main"
29
+
30
+ [project.urls]
31
+ Homepage = "https://github.com/Written2001/stash_client"
32
+ Repository = "https://github.com/Written2001/stash_client"
33
+ Issues = "https://github.com/Written2001/stash_client/issues"
34
+
35
+ [tool.setuptools.packages.find]
36
+ where = ["src"]
37
+
38
+ [tool.setuptools.package-data]
39
+ stashapp_client = ["generated/*.json"]
40
+
41
+ [tool.pytest.ini_options]
42
+ testpaths = ["tests"]
43
+
44
+ [tool.ruff]
45
+ line-length = 100
46
+
47
+ [tool.mypy]
48
+ python_version = "3.10"
49
+ strict = true
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,74 @@
1
+ """Python client for the Stash GraphQL API."""
2
+
3
+ from .client import StashClient
4
+ from .criteria import (
5
+ between,
6
+ equals,
7
+ excludes,
8
+ gql_criterion,
9
+ greater_than,
10
+ includes,
11
+ includes_all,
12
+ is_null,
13
+ less_than,
14
+ matches_regex,
15
+ not_equals,
16
+ not_matches_regex,
17
+ not_null,
18
+ stash_id,
19
+ stash_ids,
20
+ )
21
+ from .errors import (
22
+ GraphQLError,
23
+ StashConnectionError,
24
+ StashError,
25
+ StashResponseError,
26
+ TransportError,
27
+ )
28
+ from .filters import (
29
+ find_filter,
30
+ gallery_filter,
31
+ group_filter,
32
+ image_filter,
33
+ marker_filter,
34
+ performer_filter,
35
+ scene_filter,
36
+ studio_filter,
37
+ tag_filter,
38
+ )
39
+ from .mutation import MutationPlan, prepare_mutations
40
+
41
+ __all__ = [
42
+ "GraphQLError",
43
+ "MutationPlan",
44
+ "StashClient",
45
+ "StashConnectionError",
46
+ "StashError",
47
+ "StashResponseError",
48
+ "TransportError",
49
+ "between",
50
+ "equals",
51
+ "excludes",
52
+ "find_filter",
53
+ "gallery_filter",
54
+ "gql_criterion",
55
+ "greater_than",
56
+ "group_filter",
57
+ "image_filter",
58
+ "includes",
59
+ "includes_all",
60
+ "is_null",
61
+ "less_than",
62
+ "marker_filter",
63
+ "matches_regex",
64
+ "not_equals",
65
+ "not_matches_regex",
66
+ "not_null",
67
+ "performer_filter",
68
+ "prepare_mutations",
69
+ "scene_filter",
70
+ "stash_id",
71
+ "stash_ids",
72
+ "studio_filter",
73
+ "tag_filter",
74
+ ]
@@ -0,0 +1,93 @@
1
+ """Command-line entry point for schema and operation generation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ from .client import StashClient
10
+ from .codegen import attach_documents, render_fragments, render_inputs
11
+ from .introspection import introspect_schema, load_schema, save_schema
12
+ from .registry import build_registry, save_registry
13
+ from .schema_diff import compare_schemas, format_report, has_changes, report_json
14
+
15
+
16
+ def _path_options(parser: argparse.ArgumentParser) -> None:
17
+ parser.add_argument("--out-schema", type=Path, default=Path("schema.json"))
18
+ parser.add_argument("--out-registry", type=Path, default=Path("operations_registry.json"))
19
+ parser.add_argument("--out-fragments", type=Path, default=Path("fragments.py"))
20
+ parser.add_argument("--out-inputs", type=Path, default=Path("inputs.py"))
21
+
22
+
23
+ def _write_artifacts(schema: dict[str, Any], provenance: dict[str, Any], args: Any) -> None:
24
+ save_schema({"data": {"__schema": schema}}, args.out_schema)
25
+ registry = attach_documents(build_registry(schema), schema)
26
+ registry["provenance"] = provenance
27
+ save_registry(registry, args.out_registry)
28
+ args.out_fragments.write_text(render_fragments(schema), encoding="utf-8")
29
+ args.out_inputs.write_text(render_inputs(schema), encoding="utf-8")
30
+
31
+
32
+ def _verify_value(value: str) -> bool | str:
33
+ lowered = value.lower()
34
+ if lowered == "true":
35
+ return True
36
+ if lowered == "false":
37
+ return False
38
+ return value
39
+
40
+
41
+ def _parser() -> argparse.ArgumentParser:
42
+ parser = argparse.ArgumentParser(prog="stashapp-client")
43
+ commands = parser.add_subparsers(dest="command", required=True)
44
+
45
+ build = commands.add_parser("build-ops", help="build artifacts from a schema snapshot")
46
+ build.add_argument("--schema", type=Path, required=True)
47
+ _path_options(build)
48
+
49
+ sync = commands.add_parser("sync-schema", help="introspect a live server and build artifacts")
50
+ sync.add_argument("--credentials-file", type=Path, required=True)
51
+ sync.add_argument("--verify", type=_verify_value, default=True)
52
+ sync.add_argument("--timeout", type=float, default=30)
53
+ _path_options(sync)
54
+
55
+ diff = commands.add_parser("diff-schema", help="compare two schema snapshots")
56
+ diff.add_argument("--base", type=Path, required=True)
57
+ diff.add_argument("--current", type=Path, required=True)
58
+ diff.add_argument("--json", action="store_true", dest="as_json")
59
+ return parser
60
+
61
+
62
+ def main(argv: list[str] | None = None) -> None:
63
+ args = _parser().parse_args(argv)
64
+ if args.command == "build-ops":
65
+ _write_artifacts(
66
+ load_schema(args.schema),
67
+ {"source": "introspection-snapshot", "path": str(args.schema)},
68
+ args,
69
+ )
70
+ return
71
+ if args.command == "diff-schema":
72
+ report = compare_schemas(load_schema(args.base), load_schema(args.current))
73
+ print(report_json(report) if args.as_json else format_report(report))
74
+ if has_changes(report):
75
+ raise SystemExit(1)
76
+ return
77
+
78
+ client = StashClient.from_credentials_file(
79
+ args.credentials_file, verify=args.verify, timeout=args.timeout
80
+ )
81
+ try:
82
+ schema = introspect_schema(client)
83
+ finally:
84
+ client.close()
85
+ _write_artifacts(
86
+ schema,
87
+ {"source": "live-introspection", "path": str(args.credentials_file)},
88
+ args,
89
+ )
90
+
91
+
92
+ if __name__ == "__main__":
93
+ main()