valentina-python-client 3.2.0__tar.gz → 3.4.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 (86) hide show
  1. {valentina_python_client-3.2.0 → valentina_python_client-3.4.0}/PKG-INFO +5 -4
  2. valentina_python_client-3.4.0/pyproject.toml +244 -0
  3. valentina_python_client-3.2.0/pyproject.toml → valentina_python_client-3.4.0/pyproject.toml.orig +20 -17
  4. {valentina_python_client-3.2.0 → valentina_python_client-3.4.0}/src/vclient/__init__.py +2 -2
  5. {valentina_python_client-3.2.0 → valentina_python_client-3.4.0}/src/vclient/_sync/services/campaigns.py +7 -3
  6. {valentina_python_client-3.2.0 → valentina_python_client-3.4.0}/src/vclient/_sync/services/character_traits.py +227 -3
  7. {valentina_python_client-3.2.0 → valentina_python_client-3.4.0}/src/vclient/_sync/services/characters.py +103 -3
  8. {valentina_python_client-3.2.0 → valentina_python_client-3.4.0}/src/vclient/_sync/services/options.py +7 -2
  9. {valentina_python_client-3.2.0 → valentina_python_client-3.4.0}/src/vclient/constants.py +5 -0
  10. {valentina_python_client-3.2.0 → valentina_python_client-3.4.0}/src/vclient/endpoints.py +6 -0
  11. {valentina_python_client-3.2.0 → valentina_python_client-3.4.0}/src/vclient/exceptions.py +3 -1
  12. {valentina_python_client-3.2.0 → valentina_python_client-3.4.0}/src/vclient/models/__init__.py +26 -0
  13. {valentina_python_client-3.2.0 → valentina_python_client-3.4.0}/src/vclient/models/campaigns.py +4 -1
  14. valentina_python_client-3.4.0/src/vclient/models/character_trait.py +286 -0
  15. {valentina_python_client-3.2.0 → valentina_python_client-3.4.0}/src/vclient/models/characters.py +18 -2
  16. {valentina_python_client-3.2.0 → valentina_python_client-3.4.0}/src/vclient/models/companies.py +3 -0
  17. {valentina_python_client-3.2.0 → valentina_python_client-3.4.0}/src/vclient/models/dictionary.py +5 -0
  18. {valentina_python_client-3.2.0 → valentina_python_client-3.4.0}/src/vclient/models/shared.py +29 -1
  19. {valentina_python_client-3.2.0 → valentina_python_client-3.4.0}/src/vclient/services/campaigns.py +9 -2
  20. {valentina_python_client-3.2.0 → valentina_python_client-3.4.0}/src/vclient/services/character_traits.py +244 -3
  21. {valentina_python_client-3.2.0 → valentina_python_client-3.4.0}/src/vclient/services/characters.py +112 -2
  22. {valentina_python_client-3.2.0 → valentina_python_client-3.4.0}/src/vclient/services/options.py +7 -2
  23. {valentina_python_client-3.2.0 → valentina_python_client-3.4.0}/src/vclient/testing/__init__.py +10 -0
  24. {valentina_python_client-3.2.0 → valentina_python_client-3.4.0}/src/vclient/testing/_factories.py +132 -6
  25. {valentina_python_client-3.2.0 → valentina_python_client-3.4.0}/src/vclient/testing/_router.py +17 -2
  26. {valentina_python_client-3.2.0 → valentina_python_client-3.4.0}/src/vclient/testing/_routes.py +26 -0
  27. {valentina_python_client-3.2.0 → valentina_python_client-3.4.0}/src/vclient/validate_constants.py +3 -0
  28. valentina_python_client-3.2.0/src/vclient/models/character_trait.py +0 -116
  29. {valentina_python_client-3.2.0 → valentina_python_client-3.4.0}/LICENSE +0 -0
  30. {valentina_python_client-3.2.0 → valentina_python_client-3.4.0}/README.md +0 -0
  31. {valentina_python_client-3.2.0 → valentina_python_client-3.4.0}/src/vclient/_codegen.py +0 -0
  32. {valentina_python_client-3.2.0 → valentina_python_client-3.4.0}/src/vclient/_sync/__init__.py +0 -0
  33. {valentina_python_client-3.2.0 → valentina_python_client-3.4.0}/src/vclient/_sync/client.py +0 -0
  34. {valentina_python_client-3.2.0 → valentina_python_client-3.4.0}/src/vclient/_sync/registry.py +0 -0
  35. {valentina_python_client-3.2.0 → valentina_python_client-3.4.0}/src/vclient/_sync/services/__init__.py +0 -0
  36. {valentina_python_client-3.2.0 → valentina_python_client-3.4.0}/src/vclient/_sync/services/_audit_params.py +0 -0
  37. {valentina_python_client-3.2.0 → valentina_python_client-3.4.0}/src/vclient/_sync/services/base.py +0 -0
  38. {valentina_python_client-3.2.0 → valentina_python_client-3.4.0}/src/vclient/_sync/services/campaign_book_chapters.py +0 -0
  39. {valentina_python_client-3.2.0 → valentina_python_client-3.4.0}/src/vclient/_sync/services/campaign_books.py +0 -0
  40. {valentina_python_client-3.2.0 → valentina_python_client-3.4.0}/src/vclient/_sync/services/character_autogen.py +0 -0
  41. {valentina_python_client-3.2.0 → valentina_python_client-3.4.0}/src/vclient/_sync/services/character_blueprint.py +0 -0
  42. {valentina_python_client-3.2.0 → valentina_python_client-3.4.0}/src/vclient/_sync/services/companies.py +0 -0
  43. {valentina_python_client-3.2.0 → valentina_python_client-3.4.0}/src/vclient/_sync/services/developers.py +0 -0
  44. {valentina_python_client-3.2.0 → valentina_python_client-3.4.0}/src/vclient/_sync/services/dicerolls.py +0 -0
  45. {valentina_python_client-3.2.0 → valentina_python_client-3.4.0}/src/vclient/_sync/services/dictionary.py +0 -0
  46. {valentina_python_client-3.2.0 → valentina_python_client-3.4.0}/src/vclient/_sync/services/global_admin.py +0 -0
  47. {valentina_python_client-3.2.0 → valentina_python_client-3.4.0}/src/vclient/_sync/services/identity.py +0 -0
  48. {valentina_python_client-3.2.0 → valentina_python_client-3.4.0}/src/vclient/_sync/services/system.py +0 -0
  49. {valentina_python_client-3.2.0 → valentina_python_client-3.4.0}/src/vclient/_sync/services/user_lookup.py +0 -0
  50. {valentina_python_client-3.2.0 → valentina_python_client-3.4.0}/src/vclient/_sync/services/users.py +0 -0
  51. {valentina_python_client-3.2.0 → valentina_python_client-3.4.0}/src/vclient/_sync/testing/__init__.py +0 -0
  52. {valentina_python_client-3.2.0 → valentina_python_client-3.4.0}/src/vclient/_sync/testing/_client.py +0 -0
  53. {valentina_python_client-3.2.0 → valentina_python_client-3.4.0}/src/vclient/client.py +0 -0
  54. {valentina_python_client-3.2.0 → valentina_python_client-3.4.0}/src/vclient/config.py +0 -0
  55. {valentina_python_client-3.2.0 → valentina_python_client-3.4.0}/src/vclient/models/audit_logs.py +0 -0
  56. {valentina_python_client-3.2.0 → valentina_python_client-3.4.0}/src/vclient/models/books.py +0 -0
  57. {valentina_python_client-3.2.0 → valentina_python_client-3.4.0}/src/vclient/models/chapters.py +0 -0
  58. {valentina_python_client-3.2.0 → valentina_python_client-3.4.0}/src/vclient/models/character_autogen.py +0 -0
  59. {valentina_python_client-3.2.0 → valentina_python_client-3.4.0}/src/vclient/models/character_blueprint.py +0 -0
  60. {valentina_python_client-3.2.0 → valentina_python_client-3.4.0}/src/vclient/models/developers.py +0 -0
  61. {valentina_python_client-3.2.0 → valentina_python_client-3.4.0}/src/vclient/models/diceroll.py +0 -0
  62. {valentina_python_client-3.2.0 → valentina_python_client-3.4.0}/src/vclient/models/full_sheet.py +0 -0
  63. {valentina_python_client-3.2.0 → valentina_python_client-3.4.0}/src/vclient/models/global_admin.py +0 -0
  64. {valentina_python_client-3.2.0 → valentina_python_client-3.4.0}/src/vclient/models/pagination.py +0 -0
  65. {valentina_python_client-3.2.0 → valentina_python_client-3.4.0}/src/vclient/models/system.py +0 -0
  66. {valentina_python_client-3.2.0 → valentina_python_client-3.4.0}/src/vclient/models/user_lookup.py +0 -0
  67. {valentina_python_client-3.2.0 → valentina_python_client-3.4.0}/src/vclient/models/users.py +0 -0
  68. {valentina_python_client-3.2.0 → valentina_python_client-3.4.0}/src/vclient/py.typed +0 -0
  69. {valentina_python_client-3.2.0 → valentina_python_client-3.4.0}/src/vclient/registry.py +0 -0
  70. {valentina_python_client-3.2.0 → valentina_python_client-3.4.0}/src/vclient/services/__init__.py +0 -0
  71. {valentina_python_client-3.2.0 → valentina_python_client-3.4.0}/src/vclient/services/_audit_params.py +0 -0
  72. {valentina_python_client-3.2.0 → valentina_python_client-3.4.0}/src/vclient/services/base.py +0 -0
  73. {valentina_python_client-3.2.0 → valentina_python_client-3.4.0}/src/vclient/services/campaign_book_chapters.py +0 -0
  74. {valentina_python_client-3.2.0 → valentina_python_client-3.4.0}/src/vclient/services/campaign_books.py +0 -0
  75. {valentina_python_client-3.2.0 → valentina_python_client-3.4.0}/src/vclient/services/character_autogen.py +0 -0
  76. {valentina_python_client-3.2.0 → valentina_python_client-3.4.0}/src/vclient/services/character_blueprint.py +0 -0
  77. {valentina_python_client-3.2.0 → valentina_python_client-3.4.0}/src/vclient/services/companies.py +0 -0
  78. {valentina_python_client-3.2.0 → valentina_python_client-3.4.0}/src/vclient/services/developers.py +0 -0
  79. {valentina_python_client-3.2.0 → valentina_python_client-3.4.0}/src/vclient/services/dicerolls.py +0 -0
  80. {valentina_python_client-3.2.0 → valentina_python_client-3.4.0}/src/vclient/services/dictionary.py +0 -0
  81. {valentina_python_client-3.2.0 → valentina_python_client-3.4.0}/src/vclient/services/global_admin.py +0 -0
  82. {valentina_python_client-3.2.0 → valentina_python_client-3.4.0}/src/vclient/services/identity.py +0 -0
  83. {valentina_python_client-3.2.0 → valentina_python_client-3.4.0}/src/vclient/services/system.py +0 -0
  84. {valentina_python_client-3.2.0 → valentina_python_client-3.4.0}/src/vclient/services/user_lookup.py +0 -0
  85. {valentina_python_client-3.2.0 → valentina_python_client-3.4.0}/src/vclient/services/users.py +0 -0
  86. {valentina_python_client-3.2.0 → valentina_python_client-3.4.0}/src/vclient/testing/_client.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: valentina-python-client
3
- Version: 3.2.0
3
+ Version: 3.4.0
4
4
  Summary: Async Python client library for the Valentina Noir API
5
5
  Author: Nate Landau
6
6
  Author-email: Nate Landau <github@natenate.org>
@@ -15,11 +15,12 @@ License: MIT License
15
15
  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
16
16
  Classifier: Programming Language :: Python :: 3.13
17
17
  Classifier: Programming Language :: Python :: 3.14
18
- Requires-Dist: anyio>=4.14.1,<5.0.0
19
- Requires-Dist: httpx2>=2.5.0
18
+ Classifier: Programming Language :: Python :: 3.15
19
+ Requires-Dist: anyio>=4.15.1,<5.0.0
20
+ Requires-Dist: httpx2>=2.13.1
20
21
  Requires-Dist: loguru>=0.7.3
21
22
  Requires-Dist: polyfactory>=3.3.0,<4.0.0
22
- Requires-Dist: pydantic[email]>=2.13.4,<3.0.0
23
+ Requires-Dist: pydantic[email]>=2.14.1,<3.0.0
23
24
  Requires-Dist: polyfactory>=3.3.0 ; extra == 'testing'
24
25
  Requires-Python: >=3.13
25
26
  Project-URL: Homepage, https://docs.valentina-noir.com/python-api-client/
@@ -0,0 +1,244 @@
1
+ [project]
2
+ classifiers = [
3
+ "Programming Language :: Python :: 3.13",
4
+ "Programming Language :: Python :: 3.14",
5
+ "Programming Language :: Python :: 3.15",
6
+ ]
7
+ dependencies = [
8
+ "anyio>=4.15.1,<5.0.0",
9
+ "httpx2>=2.13.1",
10
+ "loguru>=0.7.3",
11
+ "polyfactory>=3.3.0, <4.0.0",
12
+ "pydantic[email]>=2.14.1,<3.0.0",
13
+ ]
14
+ description = "Async Python client library for the Valentina Noir API"
15
+ name = "valentina-python-client"
16
+ readme = "README.md"
17
+ requires-python = ">=3.13"
18
+ version = "3.4.0"
19
+
20
+ [[project.authors]]
21
+ name = "Nate Landau"
22
+ email = "github@natenate.org"
23
+
24
+ [project.license]
25
+ file = "LICENSE"
26
+
27
+ [project.optional-dependencies]
28
+ testing = ["polyfactory>=3.3.0"]
29
+
30
+ [project.urls]
31
+ Homepage = "https://docs.valentina-noir.com/python-api-client/"
32
+ Repository = "https://github.com/natelandau/valentina-python-client"
33
+
34
+ [build-system]
35
+ build-backend = "uv_build"
36
+ requires = ["uv_build>=0.13.0,<0.14"]
37
+
38
+ [tool.uv.build-backend]
39
+ module-name = "vclient"
40
+
41
+ [tool.commitizen]
42
+ bump_message = "bump(release): v$current_version → v$new_version"
43
+ changelog_merge_prerelease = true
44
+ tag_format = "v$version"
45
+ update_changelog_on_bump = true
46
+ version_files = ["src/vclient/__init__.py:__version__"]
47
+ version_provider = "uv"
48
+
49
+ [tool.coverage.report]
50
+ exclude_lines = [
51
+ "def __repr__",
52
+ 'except [\w\s\._]+ as .*:',
53
+ "if TYPE_CHECKING",
54
+ "pragma: no cover",
55
+ "raise Abort",
56
+ "raise Exit",
57
+ ]
58
+ fail_under = 0
59
+ precision = 1
60
+ show_missing = true
61
+ skip_covered = true
62
+ skip_empty = true
63
+
64
+ [tool.coverage.run]
65
+ branch = true
66
+ command_line = "--module pytest"
67
+ data_file = ".cache/coverage"
68
+ omit = ["tests/*"]
69
+ source = ["src"]
70
+
71
+ [tool.coverage.xml]
72
+ output = ".cache/coverage.xml"
73
+
74
+ [tool.pytest.ini_options]
75
+ addopts = "--color=yes --doctest-modules --strict-config --strict-markers -n auto --dist loadfile"
76
+ cache_dir = ".cache/pytest"
77
+ filterwarnings = [
78
+ "error",
79
+ "ignore:.*Pydantic.*:UserWarning",
80
+ "ignore::DeprecationWarning:",
81
+ ]
82
+ markers = ["serial"]
83
+ testpaths = ["tests"]
84
+ xfail_strict = true
85
+
86
+ [tool.ruff]
87
+ exclude = [
88
+ ".cache",
89
+ ".dev",
90
+ ".git",
91
+ ".venv",
92
+ ".worktrees",
93
+ "_build",
94
+ "_site",
95
+ "build",
96
+ "dist",
97
+ ]
98
+ fix = true
99
+ line-length = 100
100
+ output-format = "grouped"
101
+ src = [
102
+ "src",
103
+ "tests",
104
+ ]
105
+ target-version = "py313"
106
+
107
+ [tool.ruff.lint]
108
+ ignore = [
109
+ "ANN002",
110
+ "ANN003",
111
+ "ANN204",
112
+ "ANN401",
113
+ "B006",
114
+ "B008",
115
+ "COM812",
116
+ "CPY001",
117
+ "D107",
118
+ "E501",
119
+ "PLC0415",
120
+ "E711",
121
+ "E712",
122
+ "FIX002",
123
+ "S311",
124
+ "TD001",
125
+ "TD002",
126
+ "TD003",
127
+ ]
128
+ select = ["ALL"]
129
+ unfixable = [
130
+ "ERA001",
131
+ "F401",
132
+ "F841",
133
+ ]
134
+
135
+ [tool.ruff.lint.per-file-ignores]
136
+ "src/vclient/_sync/**" = [
137
+ "C416",
138
+ "C901",
139
+ "D",
140
+ "E501",
141
+ "ERA001",
142
+ "PLR0913",
143
+ "PLR2004",
144
+ "PLW0603",
145
+ "SLF001",
146
+ "TRY300",
147
+ "UP028",
148
+ ]
149
+ "scripts/*.py" = [
150
+ "INP001",
151
+ "T201",
152
+ ]
153
+ "src/vclient/services/*.py" = ["PLR0913"]
154
+ "src/vclient/validate_constants.py" = ["T201"]
155
+ "tests/**/*.py" = [
156
+ "A002",
157
+ "A003",
158
+ "ANN001",
159
+ "ANN002",
160
+ "ANN003",
161
+ "ANN201",
162
+ "ARG001",
163
+ "ARG002",
164
+ "ARG005",
165
+ "D102",
166
+ "ERA001",
167
+ "F403",
168
+ "F405",
169
+ "FBT001",
170
+ "PGH003",
171
+ "PLC0415",
172
+ "PLR0913",
173
+ "PLR0917",
174
+ "PLR2004",
175
+ "PT011",
176
+ "S101",
177
+ "SLF001",
178
+ "W292",
179
+ ]
180
+
181
+ [tool.ruff.lint.mccabe]
182
+ max-complexity = 10
183
+
184
+ [tool.ruff.lint.isort]
185
+ combine-as-imports = true
186
+
187
+ [tool.ruff.lint.pydocstyle]
188
+ convention = "google"
189
+
190
+ [tool.ruff.lint.pylint]
191
+ max-args = 6
192
+
193
+ [tool.ruff.format]
194
+ exclude = ["src/vclient/_sync"]
195
+ indent-style = "space"
196
+ line-ending = "auto"
197
+ quote-style = "double"
198
+ skip-magic-trailing-comma = false
199
+
200
+ [tool.vulture]
201
+ exclude = [
202
+ "duties.py",
203
+ "scripts/",
204
+ "tests/",
205
+ "tmp/",
206
+ ]
207
+ ignore_decorators = []
208
+ ignore_names = []
209
+ make_whitelist = false
210
+ min_confidence = 100
211
+ paths = ["src/"]
212
+ sort_by_size = true
213
+ verbose = false
214
+
215
+ [tool.ty.terminal]
216
+ error-on-warning = true
217
+
218
+ [dependency-groups]
219
+ dev = [
220
+ "commitizen>=4.19.2",
221
+ "coverage>=7.16.2",
222
+ "duty>=1.10.1",
223
+ "gate-cache>=0.6.0",
224
+ "httpx>=0.28.1",
225
+ "prek>=0.5.5",
226
+ "pytest-anyio>=0.0.0",
227
+ "pytest-clarity>=1.0.1",
228
+ "pytest-cov>=7.1.0",
229
+ "pytest-devtools>=1.3.0",
230
+ "pytest-httpx2>=1.0.0",
231
+ "pytest-mock>=3.16.0",
232
+ "pytest-repeat>=0.9.4",
233
+ "pytest-sugar>=1.1.1",
234
+ "pytest-xdist>=3.8.0",
235
+ "pytest>=9.1.1",
236
+ "respx>=0.23.1",
237
+ "ruff>=0.17.0",
238
+ "shellcheck-py>=0.11.0.1",
239
+ "ty>=0.0.86",
240
+ "typos>=1.51.1",
241
+ "vulture>=2.16",
242
+ "yamllint>=1.38.0",
243
+ ]
244
+ docs = ["zensical>=0.0.70"]
@@ -3,20 +3,21 @@
3
3
  classifiers = [
4
4
  "Programming Language :: Python :: 3.13",
5
5
  "Programming Language :: Python :: 3.14",
6
+ "Programming Language :: Python :: 3.15",
6
7
  ]
7
8
  dependencies = [
8
- "anyio>=4.14.1,<5.0.0",
9
- "httpx2>=2.5.0",
9
+ "anyio>=4.15.1,<5.0.0",
10
+ "httpx2>=2.13.1",
10
11
  "loguru>=0.7.3",
11
12
  "polyfactory>=3.3.0, <4.0.0",
12
- "pydantic[email]>=2.13.4, <3.0.0",
13
+ "pydantic[email]>=2.14.1,<3.0.0",
13
14
  ]
14
15
  description = "Async Python client library for the Valentina Noir API"
15
16
  license = { file = "LICENSE" }
16
17
  name = "valentina-python-client"
17
18
  readme = "README.md"
18
19
  requires-python = ">=3.13"
19
- version = "3.2.0"
20
+ version = "3.4.0"
20
21
 
21
22
  [project.optional-dependencies]
22
23
  testing = ["polyfactory>=3.3.0"]
@@ -27,37 +28,38 @@
27
28
 
28
29
  [build-system]
29
30
  build-backend = "uv_build"
30
- requires = ["uv_build>=0.11.15,<0.12.0"]
31
+ requires = ["uv_build>=0.13.0,<0.14"]
31
32
 
32
33
  [tool.uv.build-backend]
33
34
  module-name = "vclient"
34
35
 
35
36
  [dependency-groups]
36
37
  dev = [
37
- "commitizen>=4.16.4",
38
- "coverage>=7.15.0",
39
- "duty>=1.9.0",
38
+ "commitizen>=4.19.2",
39
+ "coverage>=7.16.2",
40
+ "duty>=1.10.1",
41
+ "gate-cache>=0.6.0",
40
42
  "httpx>=0.28.1",
41
- "prek>=0.4.8",
43
+ "prek>=0.5.5",
42
44
  "pytest-anyio>=0.0.0",
43
45
  "pytest-clarity>=1.0.1",
44
46
  "pytest-cov>=7.1.0",
45
- "pytest-devtools>=1.2.0",
47
+ "pytest-devtools>=1.3.0",
46
48
  "pytest-httpx2>=1.0.0",
47
- "pytest-mock>=3.15.1",
49
+ "pytest-mock>=3.16.0",
48
50
  "pytest-repeat>=0.9.4",
49
51
  "pytest-sugar>=1.1.1",
50
52
  "pytest-xdist>=3.8.0",
51
53
  "pytest>=9.1.1",
52
54
  "respx>=0.23.1",
53
- "ruff>=0.15.20",
55
+ "ruff>=0.17.0",
54
56
  "shellcheck-py>=0.11.0.1",
55
- "ty>=0.0.56",
56
- "typos>=1.48.0",
57
+ "ty>=0.0.86",
58
+ "typos>=1.51.1",
57
59
  "vulture>=2.16",
58
60
  "yamllint>=1.38.0",
59
- ]
60
- docs = ["zensical>=0.0.47"]
61
+ ]
62
+ docs = ["zensical>=0.0.70"]
61
63
 
62
64
  [tool.commitizen]
63
65
  bump_message = "bump(release): v$current_version → v$new_version"
@@ -168,7 +170,8 @@
168
170
  "FBT001", # Boolean-typed positional argument in function definition
169
171
  "PGH003", # Use specific rule codes when ignoring type issues
170
172
  "PLC0415", # Imports should be at top of file
171
- "PLR0913",
173
+ "PLR0913", # Too many positional arguments
174
+ "PLR0917", # Too many positional arguments
172
175
  "PLR2004",
173
176
  "PT011", # set the `match` parameter or use a more specific exception
174
177
  "S101",
@@ -31,7 +31,7 @@ _logger.add(
31
31
  # Forward TRACE too (e.g. the "Send request" line) and let the consumer's
32
32
  # stdlib logging level decide what to show; the sink must not pre-filter it.
33
33
  level="TRACE",
34
- filter=lambda record: record["name"].startswith("vclient"),
34
+ filter=lambda record: record["name"].startswith("vclient"), # ty: ignore[unresolved-attribute]
35
35
  )
36
36
 
37
37
  _logger.disable("vclient")
@@ -116,4 +116,4 @@ __all__ = (
116
116
  "users_service",
117
117
  )
118
118
 
119
- __version__ = "3.2.0"
119
+ __version__ = "3.4.0"
@@ -127,6 +127,7 @@ class SyncCampaignsService(SyncBaseService):
127
127
  request: A CampaignCreate model, OR pass fields as keyword arguments.
128
128
  **kwargs: Fields for CampaignCreate if request is not provided.
129
129
  Accepts: name (str, required), description (str | None),
130
+ in_game_date (datetime.date | None, ISO 8601 calendar date),
130
131
  desperation (int, default 0), danger (int, default 0).
131
132
 
132
133
  Returns:
@@ -154,7 +155,8 @@ class SyncCampaignsService(SyncBaseService):
154
155
  request: A CampaignUpdate model, OR pass fields as keyword arguments.
155
156
  **kwargs: Fields for CampaignUpdate if request is not provided.
156
157
  Accepts: name (str | None), description (str | None),
157
- desperation (int | None), danger (int | None).
158
+ in_game_date (datetime.date | None, ISO 8601 calendar date;
159
+ pass None to clear it), desperation (int | None), danger (int | None).
158
160
 
159
161
  Returns:
160
162
  The updated Campaign object.
@@ -166,9 +168,11 @@ class SyncCampaignsService(SyncBaseService):
166
168
  ValidationError: If the request data is invalid.
167
169
  """
168
170
  body = request if request is not None else self._validate_request(CampaignUpdate, **kwargs)
171
+ payload = body.model_dump(exclude_none=True, exclude_unset=True, mode="json")
172
+ if "in_game_date" in body.model_fields_set and body.in_game_date is None:
173
+ payload["in_game_date"] = None
169
174
  response = self._patch(
170
- self._format_endpoint(Endpoints.CAMPAIGN, campaign_id=campaign_id),
171
- json=body.model_dump(exclude_none=True, exclude_unset=True, mode="json"),
175
+ self._format_endpoint(Endpoints.CAMPAIGN, campaign_id=campaign_id), json=payload
172
176
  )
173
177
  return Campaign.model_validate(response.json())
174
178
 
@@ -5,15 +5,21 @@ from collections.abc import Iterator
5
5
  from typing import TYPE_CHECKING
6
6
 
7
7
  from vclient._sync.services.base import SyncBaseService
8
- from vclient.constants import DEFAULT_PAGE_LIMIT, TraitModifyCurrency
8
+ from vclient.constants import DEFAULT_PAGE_LIMIT, DamageType, TrackerAction, TraitModifyCurrency
9
9
  from vclient.endpoints import Endpoints
10
10
  from vclient.models import (
11
+ _TRACKER_ADAPTER,
11
12
  BulkAssignTraitResponse,
12
13
  CharacterTrait,
13
14
  CharacterTraitAdd,
15
+ CharacterTraitSpecialtyCreate,
16
+ CharacterTraitSpecialtyUpdate,
14
17
  CharacterTraitValueOptionsResponse,
15
18
  PaginatedResponse,
19
+ Tracker,
20
+ TrackerStateSet,
16
21
  TraitCreate,
22
+ _TrackerAction,
17
23
  _TraitModify,
18
24
  )
19
25
 
@@ -140,6 +146,8 @@ class SyncCharacterTraitsService(SyncBaseService):
140
146
  def delete(self, character_trait_id: str, currency: TraitModifyCurrency | None = None) -> None:
141
147
  """Delete a character trait.
142
148
 
149
+ Any specialties on the trait are removed with it and the cost of each is refunded.
150
+
143
151
  Args:
144
152
  character_trait_id: The ID of the trait to delete.
145
153
  currency: The currency to use to recoup the cost of the trait.
@@ -152,6 +160,8 @@ class SyncCharacterTraitsService(SyncBaseService):
152
160
  AuthorizationError: If you don't have access to the character.
153
161
  RequestValidationError: If the input parameters fail client-side validation.
154
162
  ValidationError: If the request data is invalid.
163
+ ConflictError: If recouping ``XP`` or ``STARTING_POINTS`` and another request
164
+ changed the trait's value first. Refetch the trait and retry.
155
165
  """
156
166
  params: dict[str, str | int] = {}
157
167
  if currency is not None:
@@ -177,6 +187,8 @@ class SyncCharacterTraitsService(SyncBaseService):
177
187
  AuthorizationError: If you don't have access to the character.
178
188
  RequestValidationError: If the input parameters fail client-side validation.
179
189
  ValidationError: If the request data is invalid.
190
+ ConflictError: If paying with ``XP`` or ``STARTING_POINTS`` and another request
191
+ changed the trait's value first. Refetch the trait and retry.
180
192
  """
181
193
  body = self._validate_request(
182
194
  CharacterTraitAdd, trait_id=trait_id, value=value, currency=currency
@@ -222,8 +234,16 @@ class SyncCharacterTraitsService(SyncBaseService):
222
234
  **kwargs: Fields for TraitCreate if request is not provided.
223
235
  Required: name (str), category_id (str), currency (TraitModifyCurrency).
224
236
  Optional: description (str | None), max_value (int), min_value (int),
225
- show_when_zero (bool), initial_cost (int | None),
226
- upgrade_cost (int | None).
237
+ show_when_zero (bool), is_rollable (bool, default True),
238
+ initial_cost (int | None), upgrade_cost (int | None),
239
+ count_based_cost_multiplier (int | None).
240
+
241
+ Returns:
242
+ The newly created CharacterTrait object.
243
+
244
+ Raises:
245
+ ConflictError: If paying with ``XP`` or ``STARTING_POINTS`` and another request
246
+ changed the trait's value first. Refetch the trait and retry.
227
247
  """
228
248
  body = request if request is not None else self._validate_request(TraitCreate, **kwargs)
229
249
  response = self._post(
@@ -259,10 +279,20 @@ class SyncCharacterTraitsService(SyncBaseService):
259
279
  ) -> CharacterTrait:
260
280
  """Change the value of a character trait.
261
281
 
282
+ Lowering an ability to ``0`` also removes its specialties and refunds the cost of
283
+ each. The value change and the refunds are applied together or not at all.
284
+
262
285
  Args:
263
286
  character_trait_id: The ID of the trait to change the value of.
264
287
  new_value: The new value of the trait.
265
288
  currency: The currency to use for the modification.
289
+
290
+ Returns:
291
+ The updated CharacterTrait object.
292
+
293
+ Raises:
294
+ ConflictError: If another request changed the trait's value after this request
295
+ read it. Nothing is spent or refunded. Refetch the trait and retry.
266
296
  """
267
297
  body = self._validate_request(_TraitModify, target_value=new_value, currency=currency)
268
298
  response = self._put(
@@ -272,3 +302,197 @@ class SyncCharacterTraitsService(SyncBaseService):
272
302
  json=body.model_dump(exclude_none=True, exclude_unset=True, mode="json"),
273
303
  )
274
304
  return CharacterTrait.model_validate(response.json())
305
+
306
+ def add_specialty(
307
+ self, character_trait_id: str, name: str, currency: TraitModifyCurrency = "NO_COST"
308
+ ) -> CharacterTrait:
309
+ """Add a specialty to an ability trait.
310
+
311
+ Specialties are only available on abilities with a value of 1 or higher. When the
312
+ company's ``specialty_xp_cost_enabled`` setting is on, a specialty paid with ``XP``
313
+ or ``STARTING_POINTS`` costs 3 points, and ``NO_COST`` follows the company's
314
+ ``permission_free_trait_changes`` setting. When the setting is off, specialties are free.
315
+
316
+ Args:
317
+ character_trait_id: The ID of the ability trait.
318
+ name: The specialty name (3-50 characters), unique on the trait ignoring case.
319
+ currency: The currency to use to pay for the specialty.
320
+
321
+ Returns:
322
+ The updated CharacterTrait object, including its specialties.
323
+
324
+ Raises:
325
+ NotFoundError: If the trait does not exist.
326
+ AuthorizationError: If you don't have access to the character.
327
+ RequestValidationError: If the input parameters fail client-side validation.
328
+ ValidationError: If the trait is not an ability, has a value of 0, already has a
329
+ specialty with this name, the character cannot afford the cost, or an
330
+ ``NPC`` or ``STORYTELLER`` character pays with anything but ``NO_COST``.
331
+ """
332
+ body = self._validate_request(CharacterTraitSpecialtyCreate, name=name, currency=currency)
333
+ response = self._post(
334
+ self._format_endpoint(
335
+ Endpoints.CHARACTER_TRAIT_SPECIALTIES, character_trait_id=character_trait_id
336
+ ),
337
+ json=body.model_dump(exclude_none=True, mode="json"),
338
+ )
339
+ return CharacterTrait.model_validate(response.json())
340
+
341
+ def update_specialty(
342
+ self, character_trait_id: str, specialty_id: str, name: str
343
+ ) -> CharacterTrait:
344
+ """Rename a specialty on an ability trait.
345
+
346
+ Renaming never changes the cost paid or the currency used.
347
+
348
+ Args:
349
+ character_trait_id: The ID of the ability trait.
350
+ specialty_id: The ID of the specialty to rename.
351
+ name: The new specialty name (3-50 characters), unique on the trait ignoring case.
352
+
353
+ Returns:
354
+ The updated CharacterTrait object, including its specialties.
355
+
356
+ Raises:
357
+ NotFoundError: If the trait or specialty does not exist, or the specialty
358
+ belongs to a different trait.
359
+ AuthorizationError: If you don't have access to the character.
360
+ RequestValidationError: If the input parameters fail client-side validation.
361
+ ValidationError: If the trait already has a specialty with this name.
362
+ """
363
+ body = self._validate_request(CharacterTraitSpecialtyUpdate, name=name)
364
+ response = self._patch(
365
+ self._format_endpoint(
366
+ Endpoints.CHARACTER_TRAIT_SPECIALTY,
367
+ character_trait_id=character_trait_id,
368
+ specialty_id=specialty_id,
369
+ ),
370
+ json=body.model_dump(exclude_none=True, mode="json"),
371
+ )
372
+ return CharacterTrait.model_validate(response.json())
373
+
374
+ def delete_specialty(self, character_trait_id: str, specialty_id: str) -> None:
375
+ """Remove a specialty from an ability trait.
376
+
377
+ The cost paid is refunded in the currency used to buy it. An ``XP`` refund follows
378
+ the company's ``permission_recoup_xp`` setting; starting points are always refunded.
379
+
380
+ Args:
381
+ character_trait_id: The ID of the ability trait.
382
+ specialty_id: The ID of the specialty to remove.
383
+
384
+ Returns:
385
+ None
386
+
387
+ Raises:
388
+ NotFoundError: If the trait or specialty does not exist, or the specialty
389
+ belongs to a different trait.
390
+ AuthorizationError: If you don't have access to the character.
391
+ """
392
+ self._delete(
393
+ self._format_endpoint(
394
+ Endpoints.CHARACTER_TRAIT_SPECIALTY,
395
+ character_trait_id=character_trait_id,
396
+ specialty_id=specialty_id,
397
+ )
398
+ )
399
+
400
+ def get_tracker(self, character_trait_id: str) -> Tracker:
401
+ """Get the in-play tracker state of a character trait.
402
+
403
+ The same object appears as ``CharacterTrait.tracker``. Use this to refresh one
404
+ tracker without fetching the whole trait.
405
+
406
+ Args:
407
+ character_trait_id: The ID of the character trait.
408
+
409
+ Returns:
410
+ A CounterTracker, DamageTracker, or StainsTracker. Switch on ``type``.
411
+
412
+ Raises:
413
+ NotFoundError: If the trait has no tracker for the character's game version.
414
+ AuthorizationError: If you don't have access to the character.
415
+ """
416
+ response = self._get(
417
+ self._format_endpoint(
418
+ Endpoints.CHARACTER_TRAIT_TRACKER, character_trait_id=character_trait_id
419
+ )
420
+ )
421
+ return _TRACKER_ADAPTER.validate_python(response.json())
422
+
423
+ def set_tracker(
424
+ self, character_trait_id: str, request: TrackerStateSet | None = None, **kwargs
425
+ ) -> Tracker:
426
+ """Replace the full state of a trait tracker, such as to correct a mistake.
427
+
428
+ Send only the fields for the tracker's type: ``current`` for counters (required),
429
+ ``superficial`` and ``aggravated`` for damage tracks, ``stains`` for stains. Omitted
430
+ fields reset to 0, so an empty request clears a damage or stains tracker. Use
431
+ ``apply_tracker_action`` for in-play changes instead.
432
+
433
+ Args:
434
+ character_trait_id: The ID of the character trait.
435
+ request: A TrackerStateSet model, OR pass fields as keyword arguments.
436
+ **kwargs: Fields for TrackerStateSet if request is not provided.
437
+ Accepts: current, superficial, aggravated, stains (int >= 0).
438
+
439
+ Returns:
440
+ The updated tracker.
441
+
442
+ Raises:
443
+ NotFoundError: If the trait has no tracker for the character's game version.
444
+ AuthorizationError: If you are not the character's player or a storyteller.
445
+ RequestValidationError: If a value is negative.
446
+ ValidationError: If a field does not fit the tracker type, a counter has no
447
+ ``current``, or a value exceeds the tracker size.
448
+ """
449
+ body = request if request is not None else self._validate_request(TrackerStateSet, **kwargs)
450
+ response = self._put(
451
+ self._format_endpoint(
452
+ Endpoints.CHARACTER_TRAIT_TRACKER, character_trait_id=character_trait_id
453
+ ),
454
+ json=body.model_dump(exclude_none=True, mode="json"),
455
+ )
456
+ return _TRACKER_ADAPTER.validate_python(response.json())
457
+
458
+ def apply_tracker_action(
459
+ self,
460
+ character_trait_id: str,
461
+ action: TrackerAction,
462
+ *,
463
+ amount: int = 1,
464
+ damage_type: DamageType | None = None,
465
+ ) -> Tracker:
466
+ """Apply one in-play action to a trait tracker, such as spending Willpower.
467
+
468
+ The server applies the game rules for the character's game version. Which actions
469
+ and damage types fit each tracker is listed in ``characters.TrackerRules`` from
470
+ ``options.get_options()``. A STAINS response reports overflow into Willpower in
471
+ ``willpower_damage_applied``.
472
+
473
+ Args:
474
+ character_trait_id: The ID of the character trait.
475
+ action: The action to apply.
476
+ amount: How many points to apply. Must be at least 1.
477
+ damage_type: The damage type for ``damage`` and ``heal`` on damage tracks.
478
+
479
+ Returns:
480
+ The updated tracker.
481
+
482
+ Raises:
483
+ NotFoundError: If the trait has no tracker for the character's game version.
484
+ AuthorizationError: If you are not the character's player or a storyteller.
485
+ RequestValidationError: If ``amount`` is less than 1.
486
+ ValidationError: If the action or damage type does not fit the tracker, a
487
+ damage type is missing or not allowed, or a spend exceeds the current value.
488
+ """
489
+ body = self._validate_request(
490
+ _TrackerAction, action=action, amount=amount, damage_type=damage_type
491
+ )
492
+ response = self._post(
493
+ self._format_endpoint(
494
+ Endpoints.CHARACTER_TRAIT_TRACKER_ACTIONS, character_trait_id=character_trait_id
495
+ ),
496
+ json=body.model_dump(exclude_none=True, mode="json"),
497
+ )
498
+ return _TRACKER_ADAPTER.validate_python(response.json())