stashapp-cli 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.
Files changed (41) hide show
  1. stashapp_cli-0.1.0/LICENSE +21 -0
  2. stashapp_cli-0.1.0/PKG-INFO +208 -0
  3. stashapp_cli-0.1.0/README.md +184 -0
  4. stashapp_cli-0.1.0/pyproject.toml +56 -0
  5. stashapp_cli-0.1.0/pyproject.toml.orig +51 -0
  6. stashapp_cli-0.1.0/src/stashapp_cli/__init__.py +2 -0
  7. stashapp_cli-0.1.0/src/stashapp_cli/cli.py +149 -0
  8. stashapp_cli-0.1.0/src/stashapp_cli/commands/__init__.py +39 -0
  9. stashapp_cli-0.1.0/src/stashapp_cli/commands/_common.py +440 -0
  10. stashapp_cli-0.1.0/src/stashapp_cli/commands/backup.py +70 -0
  11. stashapp_cli-0.1.0/src/stashapp_cli/commands/generate.py +88 -0
  12. stashapp_cli-0.1.0/src/stashapp_cli/commands/graphql.py +212 -0
  13. stashapp_cli-0.1.0/src/stashapp_cli/commands/group.py +137 -0
  14. stashapp_cli-0.1.0/src/stashapp_cli/commands/introspect.py +35 -0
  15. stashapp_cli-0.1.0/src/stashapp_cli/commands/job.py +122 -0
  16. stashapp_cli-0.1.0/src/stashapp_cli/commands/performer.py +368 -0
  17. stashapp_cli-0.1.0/src/stashapp_cli/commands/scan.py +92 -0
  18. stashapp_cli-0.1.0/src/stashapp_cli/commands/scene.py +405 -0
  19. stashapp_cli-0.1.0/src/stashapp_cli/commands/scrape.py +133 -0
  20. stashapp_cli-0.1.0/src/stashapp_cli/commands/stashbox.py +173 -0
  21. stashapp_cli-0.1.0/src/stashapp_cli/commands/studio.py +227 -0
  22. stashapp_cli-0.1.0/src/stashapp_cli/commands/tag.py +694 -0
  23. stashapp_cli-0.1.0/src/stashapp_cli/core.py +165 -0
  24. stashapp_cli-0.1.0/src/stashapp_cli/images.py +72 -0
  25. stashapp_cli-0.1.0/src/stashapp_cli/introspect.py +343 -0
  26. stashapp_cli-0.1.0/src/stashapp_cli/py.typed +0 -0
  27. stashapp_cli-0.1.0/src/stashapp_cli/queries/__init__.py +40 -0
  28. stashapp_cli-0.1.0/src/stashapp_cli/queries/_get.py +27 -0
  29. stashapp_cli-0.1.0/src/stashapp_cli/queries/_image_flags.py +30 -0
  30. stashapp_cli-0.1.0/src/stashapp_cli/queries/backup.py +20 -0
  31. stashapp_cli-0.1.0/src/stashapp_cli/queries/group.py +117 -0
  32. stashapp_cli-0.1.0/src/stashapp_cli/queries/job.py +29 -0
  33. stashapp_cli-0.1.0/src/stashapp_cli/queries/metadata.py +42 -0
  34. stashapp_cli-0.1.0/src/stashapp_cli/queries/performer.py +179 -0
  35. stashapp_cli-0.1.0/src/stashapp_cli/queries/scene.py +139 -0
  36. stashapp_cli-0.1.0/src/stashapp_cli/queries/scrape.py +183 -0
  37. stashapp_cli-0.1.0/src/stashapp_cli/queries/sources.py +76 -0
  38. stashapp_cli-0.1.0/src/stashapp_cli/queries/stashbox.py +67 -0
  39. stashapp_cli-0.1.0/src/stashapp_cli/queries/studio.py +145 -0
  40. stashapp_cli-0.1.0/src/stashapp_cli/queries/tag.py +244 -0
  41. stashapp_cli-0.1.0/src/stashapp_cli/sources.py +116 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 razputingh
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,208 @@
1
+ Metadata-Version: 2.4
2
+ Name: stashapp-cli
3
+ Version: 0.1.0
4
+ Summary: Command-line client for the Stash GraphQL API
5
+ Keywords: stash,graphql,cli
6
+ Author: razputingh
7
+ Author-email: razputingh <331436478+razputingh@users.noreply.github.com>
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Environment :: Console
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3 :: Only
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Typing :: Typed
18
+ Requires-Python: >=3.13
19
+ Project-URL: Homepage, https://github.com/razputingh/stashapp-cli
20
+ Project-URL: Documentation, https://github.com/razputingh/stashapp-cli/tree/main/docs
21
+ Project-URL: Repository, https://github.com/razputingh/stashapp-cli
22
+ Project-URL: Issues, https://github.com/razputingh/stashapp-cli/issues
23
+ Description-Content-Type: text/markdown
24
+
25
+ # stashapp-cli
26
+
27
+ stashapp-cli is a command-line tool for [Stash](https://github.com/stashapp/stash),
28
+ the self-hosted organizer for your media library. It reads and changes your
29
+ Stash library through the GraphQL API of Stash. Each command prints JSON, so
30
+ you can use stashapp-cli in shell scripts and with tools such as `jq`.
31
+
32
+ stashapp-cli is not part of the Stash project. The Stash developers do not make
33
+ or support it.
34
+
35
+ ## What it does
36
+
37
+ - Find, create, and update studios, tags, groups, scenes, and performers.
38
+ - Merge duplicate tags and performers, and delete tags. A dry run first
39
+ reports what the change will remove.
40
+ - Scrape metadata from stash-boxes and installed scrapers.
41
+ - Start scan, generate, and identify jobs, and wait for them to end.
42
+ - Back up the Stash database, and submit scene fingerprints to a stash-box.
43
+ - Send a GraphQL query or mutation that you write.
44
+
45
+ A stash-box is a shared database of scene and performer metadata, such as
46
+ StashDB.
47
+
48
+ ## Requirements
49
+
50
+ - Python `3.13` or later.
51
+ - A Stash server and an API key for it. stashapp-cli is tested with Stash `v0.31.1`.
52
+ - Optional: [jq](https://jqlang.org/), to read the JSON output in scripts. The
53
+ examples in the guides use it.
54
+
55
+ ## Install
56
+
57
+ Install stashapp-cli from PyPI with [uv](https://docs.astral.sh/uv/) or
58
+ [pipx](https://pipx.pypa.io/). The package is named `stashapp-cli`. The
59
+ command it installs is `stashapp-cli`:
60
+
61
+ ```bash
62
+ uv tool install stashapp-cli
63
+ pipx install stashapp-cli
64
+ ```
65
+
66
+ To install the latest code from GitHub, run:
67
+
68
+ ```bash
69
+ uv tool install git+https://github.com/razputingh/stashapp-cli
70
+ ```
71
+
72
+ ## Connect to Stash
73
+
74
+ stashapp-cli reads two environment variables. It has no credential flags or
75
+ credential files.
76
+
77
+ ```bash
78
+ export STASH_URL=http://localhost:9999
79
+ export STASH_API_KEY=<key>
80
+ ```
81
+
82
+ `STASH_URL` is the base URL of your Stash server. You make an API key in Stash
83
+ under Settings > Security > Authentication. If a variable is not set, the
84
+ command stops with exit status 4. For the other exit statuses, read
85
+ [Errors and exit statuses][errors].
86
+
87
+ ## Quick start
88
+
89
+ Make sure that your Stash server has every name that stashapp-cli uses. Run this
90
+ command before the first write to a server, and after you upgrade Stash:
91
+
92
+ ```bash
93
+ stashapp-cli introspect
94
+ ```
95
+
96
+ Find a tag by its name or alias:
97
+
98
+ ```bash
99
+ stashapp-cli tag find --name hd
100
+ ```
101
+
102
+ Make the tag if it does not exist. Without `--apply`, the command only reports
103
+ what it would send:
104
+
105
+ ```bash
106
+ stashapp-cli tag find-or-create hd
107
+ ```
108
+
109
+ ```json
110
+ {"applied": false, "operation": "tagCreate", "input": {"name": "hd"}}
111
+ ```
112
+
113
+ Add `--apply` to make the change:
114
+
115
+ ```bash
116
+ stashapp-cli tag find-or-create hd --apply
117
+ ```
118
+
119
+ Each command has a `--help` output with its options and examples:
120
+
121
+ ```bash
122
+ stashapp-cli --help
123
+ stashapp-cli tag --help
124
+ stashapp-cli tag merge --help
125
+ ```
126
+
127
+ ## Changes are dry runs by default
128
+
129
+ Each command has one of three kinds. The `--help` output of a command states
130
+ its kind.
131
+
132
+ | Kind | What the command does |
133
+ | --- | --- |
134
+ | `read` | It never changes Stash. |
135
+ | `write` | It changes Stash only with `--apply`. Else it reports the change. |
136
+ | `destructive` | It can make a change that you cannot undo. |
137
+
138
+ A destructive change needs both `--apply` and `--force`. You can put `--apply`
139
+ anywhere in the command. Put `--force` after the command name, for example
140
+ `stashapp-cli tag merge --destination 3 --source 7 --apply --force`.
141
+
142
+ Before you merge or delete, back up the Stash database with
143
+ `stashapp-cli backup --apply`.
144
+
145
+ Do not use `--input` to add one tag, URL, or alias. A list in `--input`
146
+ replaces the whole list in Stash, so the other values are removed. Use the add
147
+ and remove options instead, for example
148
+ `stashapp-cli scene update 812 --add-tag-id 55`.
149
+
150
+ For the full rules, read [Writes are dry runs by default][writes] and
151
+ [Updates replace lists][lists].
152
+
153
+ ## Commands
154
+
155
+ Each command is a group followed by an action, for example `tag merge`. The
156
+ [command reference][commands] has a table of all commands and their kinds.
157
+
158
+ | Group | What it does |
159
+ | --- | --- |
160
+ | [`studio`][studio-cmd] | Get, find, create, and update studios. |
161
+ | [`tag`][tag-cmd] | Get, find, create, update, delete, and merge tags. |
162
+ | [`group`][group-cmd] | Get, find, create, and update groups, for example films. |
163
+ | [`scene`][scene-cmd] | Get, find, and update scenes, and start identify jobs. |
164
+ | [`performer`][performer-cmd] | Get, find, create, update, and merge performers. |
165
+ | [`scrape`][scrape-cmd] | Get metadata from stash-boxes, scrapers, and URLs. |
166
+ | [`scan`, `generate`, `job`][jobs-cmd] | Start scan and generate jobs, and wait for jobs. |
167
+ | [`backup`][backup-cmd] | Back up the Stash database. |
168
+ | [`stashbox`][stashbox-cmd] | Submit scene fingerprints to a stash-box. |
169
+ | [`introspect`, `graphql`][graphql-cmd] | Check the schema, and send your own GraphQL. |
170
+
171
+ ## Documentation
172
+
173
+ - [Usage][usage]: global options, input values, find filters, dry runs, and
174
+ list updates.
175
+ - [Command reference][commands]: the behavior of each command.
176
+ - [Guides][guides]: step-by-step tasks, such as how to merge duplicate tags.
177
+ - [Errors and exit statuses][errors]: the JSON error object, the error codes,
178
+ and the exit statuses.
179
+ - [Glossary][glossary]: the terms that these documents use.
180
+
181
+ ## Contributing
182
+
183
+ Bug reports and pull requests are welcome. Read [CONTRIBUTING.md][contributing]
184
+ for how to run the tests and how to add a command.
185
+
186
+ ## License
187
+
188
+ stashapp-cli is released under the [MIT License][license].
189
+
190
+ [writes]: docs/usage.md#writes-are-dry-runs-by-default
191
+ [lists]: docs/usage.md#updates-replace-lists
192
+ [commands]: docs/commands/README.md
193
+ [studio-cmd]: docs/commands/studio.md
194
+ [tag-cmd]: docs/commands/tag.md
195
+ [group-cmd]: docs/commands/group.md
196
+ [scene-cmd]: docs/commands/scene.md
197
+ [performer-cmd]: docs/commands/performer.md
198
+ [scrape-cmd]: docs/commands/scrape.md
199
+ [jobs-cmd]: docs/commands/jobs.md
200
+ [backup-cmd]: docs/commands/backup.md
201
+ [stashbox-cmd]: docs/commands/stashbox.md
202
+ [graphql-cmd]: docs/commands/graphql.md
203
+ [usage]: docs/usage.md
204
+ [guides]: docs/guides/README.md
205
+ [errors]: docs/errors.md
206
+ [glossary]: docs/glossary.md
207
+ [contributing]: CONTRIBUTING.md
208
+ [license]: LICENSE
@@ -0,0 +1,184 @@
1
+ # stashapp-cli
2
+
3
+ stashapp-cli is a command-line tool for [Stash](https://github.com/stashapp/stash),
4
+ the self-hosted organizer for your media library. It reads and changes your
5
+ Stash library through the GraphQL API of Stash. Each command prints JSON, so
6
+ you can use stashapp-cli in shell scripts and with tools such as `jq`.
7
+
8
+ stashapp-cli is not part of the Stash project. The Stash developers do not make
9
+ or support it.
10
+
11
+ ## What it does
12
+
13
+ - Find, create, and update studios, tags, groups, scenes, and performers.
14
+ - Merge duplicate tags and performers, and delete tags. A dry run first
15
+ reports what the change will remove.
16
+ - Scrape metadata from stash-boxes and installed scrapers.
17
+ - Start scan, generate, and identify jobs, and wait for them to end.
18
+ - Back up the Stash database, and submit scene fingerprints to a stash-box.
19
+ - Send a GraphQL query or mutation that you write.
20
+
21
+ A stash-box is a shared database of scene and performer metadata, such as
22
+ StashDB.
23
+
24
+ ## Requirements
25
+
26
+ - Python `3.13` or later.
27
+ - A Stash server and an API key for it. stashapp-cli is tested with Stash `v0.31.1`.
28
+ - Optional: [jq](https://jqlang.org/), to read the JSON output in scripts. The
29
+ examples in the guides use it.
30
+
31
+ ## Install
32
+
33
+ Install stashapp-cli from PyPI with [uv](https://docs.astral.sh/uv/) or
34
+ [pipx](https://pipx.pypa.io/). The package is named `stashapp-cli`. The
35
+ command it installs is `stashapp-cli`:
36
+
37
+ ```bash
38
+ uv tool install stashapp-cli
39
+ pipx install stashapp-cli
40
+ ```
41
+
42
+ To install the latest code from GitHub, run:
43
+
44
+ ```bash
45
+ uv tool install git+https://github.com/razputingh/stashapp-cli
46
+ ```
47
+
48
+ ## Connect to Stash
49
+
50
+ stashapp-cli reads two environment variables. It has no credential flags or
51
+ credential files.
52
+
53
+ ```bash
54
+ export STASH_URL=http://localhost:9999
55
+ export STASH_API_KEY=<key>
56
+ ```
57
+
58
+ `STASH_URL` is the base URL of your Stash server. You make an API key in Stash
59
+ under Settings > Security > Authentication. If a variable is not set, the
60
+ command stops with exit status 4. For the other exit statuses, read
61
+ [Errors and exit statuses][errors].
62
+
63
+ ## Quick start
64
+
65
+ Make sure that your Stash server has every name that stashapp-cli uses. Run this
66
+ command before the first write to a server, and after you upgrade Stash:
67
+
68
+ ```bash
69
+ stashapp-cli introspect
70
+ ```
71
+
72
+ Find a tag by its name or alias:
73
+
74
+ ```bash
75
+ stashapp-cli tag find --name hd
76
+ ```
77
+
78
+ Make the tag if it does not exist. Without `--apply`, the command only reports
79
+ what it would send:
80
+
81
+ ```bash
82
+ stashapp-cli tag find-or-create hd
83
+ ```
84
+
85
+ ```json
86
+ {"applied": false, "operation": "tagCreate", "input": {"name": "hd"}}
87
+ ```
88
+
89
+ Add `--apply` to make the change:
90
+
91
+ ```bash
92
+ stashapp-cli tag find-or-create hd --apply
93
+ ```
94
+
95
+ Each command has a `--help` output with its options and examples:
96
+
97
+ ```bash
98
+ stashapp-cli --help
99
+ stashapp-cli tag --help
100
+ stashapp-cli tag merge --help
101
+ ```
102
+
103
+ ## Changes are dry runs by default
104
+
105
+ Each command has one of three kinds. The `--help` output of a command states
106
+ its kind.
107
+
108
+ | Kind | What the command does |
109
+ | --- | --- |
110
+ | `read` | It never changes Stash. |
111
+ | `write` | It changes Stash only with `--apply`. Else it reports the change. |
112
+ | `destructive` | It can make a change that you cannot undo. |
113
+
114
+ A destructive change needs both `--apply` and `--force`. You can put `--apply`
115
+ anywhere in the command. Put `--force` after the command name, for example
116
+ `stashapp-cli tag merge --destination 3 --source 7 --apply --force`.
117
+
118
+ Before you merge or delete, back up the Stash database with
119
+ `stashapp-cli backup --apply`.
120
+
121
+ Do not use `--input` to add one tag, URL, or alias. A list in `--input`
122
+ replaces the whole list in Stash, so the other values are removed. Use the add
123
+ and remove options instead, for example
124
+ `stashapp-cli scene update 812 --add-tag-id 55`.
125
+
126
+ For the full rules, read [Writes are dry runs by default][writes] and
127
+ [Updates replace lists][lists].
128
+
129
+ ## Commands
130
+
131
+ Each command is a group followed by an action, for example `tag merge`. The
132
+ [command reference][commands] has a table of all commands and their kinds.
133
+
134
+ | Group | What it does |
135
+ | --- | --- |
136
+ | [`studio`][studio-cmd] | Get, find, create, and update studios. |
137
+ | [`tag`][tag-cmd] | Get, find, create, update, delete, and merge tags. |
138
+ | [`group`][group-cmd] | Get, find, create, and update groups, for example films. |
139
+ | [`scene`][scene-cmd] | Get, find, and update scenes, and start identify jobs. |
140
+ | [`performer`][performer-cmd] | Get, find, create, update, and merge performers. |
141
+ | [`scrape`][scrape-cmd] | Get metadata from stash-boxes, scrapers, and URLs. |
142
+ | [`scan`, `generate`, `job`][jobs-cmd] | Start scan and generate jobs, and wait for jobs. |
143
+ | [`backup`][backup-cmd] | Back up the Stash database. |
144
+ | [`stashbox`][stashbox-cmd] | Submit scene fingerprints to a stash-box. |
145
+ | [`introspect`, `graphql`][graphql-cmd] | Check the schema, and send your own GraphQL. |
146
+
147
+ ## Documentation
148
+
149
+ - [Usage][usage]: global options, input values, find filters, dry runs, and
150
+ list updates.
151
+ - [Command reference][commands]: the behavior of each command.
152
+ - [Guides][guides]: step-by-step tasks, such as how to merge duplicate tags.
153
+ - [Errors and exit statuses][errors]: the JSON error object, the error codes,
154
+ and the exit statuses.
155
+ - [Glossary][glossary]: the terms that these documents use.
156
+
157
+ ## Contributing
158
+
159
+ Bug reports and pull requests are welcome. Read [CONTRIBUTING.md][contributing]
160
+ for how to run the tests and how to add a command.
161
+
162
+ ## License
163
+
164
+ stashapp-cli is released under the [MIT License][license].
165
+
166
+ [writes]: docs/usage.md#writes-are-dry-runs-by-default
167
+ [lists]: docs/usage.md#updates-replace-lists
168
+ [commands]: docs/commands/README.md
169
+ [studio-cmd]: docs/commands/studio.md
170
+ [tag-cmd]: docs/commands/tag.md
171
+ [group-cmd]: docs/commands/group.md
172
+ [scene-cmd]: docs/commands/scene.md
173
+ [performer-cmd]: docs/commands/performer.md
174
+ [scrape-cmd]: docs/commands/scrape.md
175
+ [jobs-cmd]: docs/commands/jobs.md
176
+ [backup-cmd]: docs/commands/backup.md
177
+ [stashbox-cmd]: docs/commands/stashbox.md
178
+ [graphql-cmd]: docs/commands/graphql.md
179
+ [usage]: docs/usage.md
180
+ [guides]: docs/guides/README.md
181
+ [errors]: docs/errors.md
182
+ [glossary]: docs/glossary.md
183
+ [contributing]: CONTRIBUTING.md
184
+ [license]: LICENSE
@@ -0,0 +1,56 @@
1
+ [project]
2
+ name = "stashapp-cli"
3
+ version = "0.1.0"
4
+ description = "Command-line client for the Stash GraphQL API"
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ license-files = ["LICENSE"]
8
+ keywords = [
9
+ "stash",
10
+ "graphql",
11
+ "cli",
12
+ ]
13
+ classifiers = [
14
+ "Development Status :: 3 - Alpha",
15
+ "Environment :: Console",
16
+ "Intended Audience :: Developers",
17
+ "Operating System :: OS Independent",
18
+ "Programming Language :: Python :: 3",
19
+ "Programming Language :: Python :: 3 :: Only",
20
+ "Programming Language :: Python :: 3.13",
21
+ "Typing :: Typed",
22
+ ]
23
+ requires-python = ">=3.13"
24
+ dependencies = []
25
+
26
+ [[project.authors]]
27
+ name = "razputingh"
28
+ email = "331436478+razputingh@users.noreply.github.com"
29
+
30
+ [project.urls]
31
+ Homepage = "https://github.com/razputingh/stashapp-cli"
32
+ Documentation = "https://github.com/razputingh/stashapp-cli/tree/main/docs"
33
+ Repository = "https://github.com/razputingh/stashapp-cli"
34
+ Issues = "https://github.com/razputingh/stashapp-cli/issues"
35
+
36
+ [project.scripts]
37
+ stashapp-cli = "stashapp_cli.cli:main"
38
+
39
+ [build-system]
40
+ requires = ["uv_build>=0.11.16,<0.12.0"]
41
+ build-backend = "uv_build"
42
+
43
+ [dependency-groups]
44
+ dev = [
45
+ "pytest>=9.1.1",
46
+ "ruff>=0.16.9",
47
+ "ty>=0.0.84",
48
+ ]
49
+
50
+ [tool.pytest.ini_options]
51
+ testpaths = ["tests"]
52
+
53
+ [tool.ruff]
54
+ target-version = "py313"
55
+ src = ["src"]
56
+ exclude = ["*.md"]
@@ -0,0 +1,51 @@
1
+ [project]
2
+ name = "stashapp-cli"
3
+ version = "0.1.0"
4
+ description = "Command-line client for the Stash GraphQL API"
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ license-files = ["LICENSE"]
8
+ authors = [
9
+ { name = "razputingh", email = "331436478+razputingh@users.noreply.github.com" }
10
+ ]
11
+ keywords = ["stash", "graphql", "cli"]
12
+ classifiers = [
13
+ "Development Status :: 3 - Alpha",
14
+ "Environment :: Console",
15
+ "Intended Audience :: Developers",
16
+ "Operating System :: OS Independent",
17
+ "Programming Language :: Python :: 3",
18
+ "Programming Language :: Python :: 3 :: Only",
19
+ "Programming Language :: Python :: 3.13",
20
+ "Typing :: Typed",
21
+ ]
22
+ requires-python = ">=3.13"
23
+ dependencies = []
24
+
25
+ [project.urls]
26
+ Homepage = "https://github.com/razputingh/stashapp-cli"
27
+ Documentation = "https://github.com/razputingh/stashapp-cli/tree/main/docs"
28
+ Repository = "https://github.com/razputingh/stashapp-cli"
29
+ Issues = "https://github.com/razputingh/stashapp-cli/issues"
30
+
31
+ [project.scripts]
32
+ stashapp-cli = "stashapp_cli.cli:main"
33
+
34
+ [build-system]
35
+ requires = ["uv_build>=0.11.16,<0.12.0"]
36
+ build-backend = "uv_build"
37
+
38
+ [dependency-groups]
39
+ dev = [
40
+ "pytest>=9.1.1",
41
+ "ruff>=0.16.9",
42
+ "ty>=0.0.84",
43
+ ]
44
+
45
+ [tool.pytest.ini_options]
46
+ testpaths = ["tests"]
47
+
48
+ [tool.ruff]
49
+ target-version = "py313"
50
+ src = ["src"]
51
+ exclude = ["*.md"]
@@ -0,0 +1,2 @@
1
+ def hello() -> str:
2
+ return "Hello from stashapp-cli!"
@@ -0,0 +1,149 @@
1
+ """stashapp-cli: inspect and modify Stash's GraphQL entities directly.
2
+
3
+ Layers: a raw `graphql` escape hatch, generic per-resource `find`/`get`/
4
+ `create`/`update` commands, and named platform-quirk commands (`tag
5
+ find-or-create`, `scrape group-url`/`scene-url`). Every mutating command is
6
+ gated by the global `--apply` flag (dry-run by default); destructive commands
7
+ (`performer merge`, and raw GraphQL *Destroy/*Delete/*Merge, ...) additionally
8
+ require `--force`. Results print as JSON on stdout; human-facing status and
9
+ errors print to stderr.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import argparse
15
+ import importlib.metadata
16
+ import sys
17
+ from collections.abc import Iterator
18
+ from pathlib import Path
19
+
20
+ from . import commands, core
21
+ from .commands._common import add_examples, positive_seconds
22
+
23
+
24
+ def _package_version() -> str:
25
+ """Return installed metadata, with a source-tree fallback for development."""
26
+ try:
27
+ return importlib.metadata.version("stashapp-cli")
28
+ except importlib.metadata.PackageNotFoundError:
29
+ pyproject = Path(__file__).parents[2] / "pyproject.toml"
30
+ try:
31
+ import tomllib
32
+
33
+ return tomllib.loads(pyproject.read_text())["project"]["version"]
34
+ except (OSError, KeyError, tomllib.TOMLDecodeError):
35
+ return "unknown"
36
+
37
+
38
+ def _emit_error(error: core.CliError, json_errors: bool) -> None:
39
+ if json_errors:
40
+ core.output(
41
+ {"error": {"code": error.code, "message": str(error), **error.details}}
42
+ )
43
+ else:
44
+ print(str(error), file=sys.stderr)
45
+
46
+
47
+ def _normalize_apply(argv: list[str]) -> list[str]:
48
+ """Normalize global arguments that argparse cannot otherwise disambiguate."""
49
+ normalized = list(argv)
50
+ for index, value in enumerate(normalized[:-1]):
51
+ if value == "--timeout" and normalized[index + 1].lower() in {
52
+ "-inf",
53
+ "-infinity",
54
+ }:
55
+ normalized[index : index + 2] = [f"--timeout={normalized[index + 1]}"]
56
+ break
57
+ if normalized.count("--apply") != 1:
58
+ return normalized
59
+ index = normalized.index("--apply")
60
+ if index == 0:
61
+ return normalized
62
+ return ["--apply", *normalized[:index], *normalized[index + 1 :]]
63
+
64
+
65
+ # --- entrypoint ------------------------------------------------------------
66
+
67
+
68
+ def iter_leaf_commands(
69
+ parser: argparse.ArgumentParser, path: tuple[str, ...] = ()
70
+ ) -> Iterator[tuple[tuple[str, ...], argparse.ArgumentParser]]:
71
+ """Yield `(path, parser)` for every leaf command below `parser`."""
72
+ subparsers = [
73
+ action
74
+ for action in parser._actions
75
+ if isinstance(action, argparse._SubParsersAction)
76
+ ]
77
+ if not subparsers:
78
+ yield path, parser
79
+ return
80
+ for action in subparsers:
81
+ for name, child in action.choices.items():
82
+ yield from iter_leaf_commands(child, (*path, name))
83
+
84
+
85
+ def build_parser() -> argparse.ArgumentParser:
86
+ parser = argparse.ArgumentParser(prog="stashapp-cli", description=__doc__)
87
+ parser.add_argument(
88
+ "--apply",
89
+ action="store_true",
90
+ help=(
91
+ "Perform mutating commands instead of reporting a dry run "
92
+ "(default: dry-run; inert on read-only commands)."
93
+ ),
94
+ )
95
+ parser.add_argument(
96
+ "--json-errors",
97
+ action="store_true",
98
+ help="Write expected failures as one JSON error envelope on stdout.",
99
+ )
100
+ parser.add_argument(
101
+ "--timeout",
102
+ type=positive_seconds,
103
+ default=core.DEFAULT_TIMEOUT,
104
+ metavar="SECONDS",
105
+ help="Maximum seconds for each Stash request (default: 30).",
106
+ )
107
+ parser.add_argument("--version", action="version", version=_package_version())
108
+ subparsers = parser.add_subparsers(dest="command", required=True)
109
+ for module in commands.COMMAND_MODULES:
110
+ module.register(subparsers)
111
+ add_examples(
112
+ parser,
113
+ "export STASH_URL=http://localhost:9999; export STASH_API_KEY",
114
+ "stashapp-cli studio find --name ExampleStudio",
115
+ 'stashapp-cli group create --input \'{"name": "Film"}\'',
116
+ "stashapp-cli group create --input @group.json --apply",
117
+ "stashapp-cli --json-errors --timeout 5 scene get 1",
118
+ )
119
+ parser.epilog = (parser.epilog or "") + (
120
+ "\n\nExit statuses: 0 success; 1 runtime or GraphQL failure; "
121
+ "2 usage or invalid input (including destructive_mutation: a destructive\n"
122
+ "command sent with --apply but without --force); "
123
+ "3 Stash endpoint returned HTTP 404; "
124
+ "4 missing credential or authentication failure."
125
+ )
126
+ return parser
127
+
128
+
129
+ def main(argv: list[str] | None = None) -> None:
130
+ parser = build_parser()
131
+ args = parser.parse_args(
132
+ _normalize_apply(list(argv))
133
+ if argv is not None
134
+ else _normalize_apply(sys.argv[1:])
135
+ )
136
+ try:
137
+ with core.request_timeout(args.timeout):
138
+ result = args.func(args)
139
+ except core.CliError as error:
140
+ _emit_error(error, args.json_errors)
141
+ sys.exit(error.exit_code)
142
+ except Exception as error: # noqa: BLE001
143
+ _emit_error(core.CliError(f"Unexpected error: {error}", 1), args.json_errors)
144
+ sys.exit(1)
145
+ core.output(result)
146
+
147
+
148
+ if __name__ == "__main__":
149
+ main()