valentina-python-client 1.1.1__tar.gz → 1.2.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.
- {valentina_python_client-1.1.1 → valentina_python_client-1.2.0}/PKG-INFO +23 -1
- {valentina_python_client-1.1.1 → valentina_python_client-1.2.0}/README.md +22 -0
- {valentina_python_client-1.1.1 → valentina_python_client-1.2.0}/pyproject.toml +7 -2
- {valentina_python_client-1.1.1 → valentina_python_client-1.2.0}/src/vclient/client.py +38 -8
- {valentina_python_client-1.1.1 → valentina_python_client-1.2.0}/src/vclient/constants.py +4 -0
- {valentina_python_client-1.1.1 → valentina_python_client-1.2.0}/src/vclient/endpoints.py +3 -3
- {valentina_python_client-1.1.1 → valentina_python_client-1.2.0}/src/vclient/models/users.py +1 -1
- {valentina_python_client-1.1.1 → valentina_python_client-1.2.0}/src/vclient/services/character_blueprint.py +1 -1
- {valentina_python_client-1.1.1 → valentina_python_client-1.2.0}/src/vclient/services/character_traits.py +1 -1
- {valentina_python_client-1.1.1 → valentina_python_client-1.2.0}/src/vclient/services/users.py +9 -3
- valentina_python_client-1.2.0/src/vclient/validate_constants.py +237 -0
- {valentina_python_client-1.1.1 → valentina_python_client-1.2.0}/LICENSE +0 -0
- {valentina_python_client-1.1.1 → valentina_python_client-1.2.0}/src/vclient/__init__.py +0 -0
- {valentina_python_client-1.1.1 → valentina_python_client-1.2.0}/src/vclient/config.py +0 -0
- {valentina_python_client-1.1.1 → valentina_python_client-1.2.0}/src/vclient/exceptions.py +0 -0
- {valentina_python_client-1.1.1 → valentina_python_client-1.2.0}/src/vclient/models/__init__.py +0 -0
- {valentina_python_client-1.1.1 → valentina_python_client-1.2.0}/src/vclient/models/books.py +0 -0
- {valentina_python_client-1.1.1 → valentina_python_client-1.2.0}/src/vclient/models/campaigns.py +0 -0
- {valentina_python_client-1.1.1 → valentina_python_client-1.2.0}/src/vclient/models/chapters.py +0 -0
- {valentina_python_client-1.1.1 → valentina_python_client-1.2.0}/src/vclient/models/character_autogen.py +0 -0
- {valentina_python_client-1.1.1 → valentina_python_client-1.2.0}/src/vclient/models/character_blueprint.py +0 -0
- {valentina_python_client-1.1.1 → valentina_python_client-1.2.0}/src/vclient/models/character_trait.py +0 -0
- {valentina_python_client-1.1.1 → valentina_python_client-1.2.0}/src/vclient/models/characters.py +0 -0
- {valentina_python_client-1.1.1 → valentina_python_client-1.2.0}/src/vclient/models/companies.py +0 -0
- {valentina_python_client-1.1.1 → valentina_python_client-1.2.0}/src/vclient/models/developers.py +0 -0
- {valentina_python_client-1.1.1 → valentina_python_client-1.2.0}/src/vclient/models/diceroll.py +0 -0
- {valentina_python_client-1.1.1 → valentina_python_client-1.2.0}/src/vclient/models/dictionary.py +0 -0
- {valentina_python_client-1.1.1 → valentina_python_client-1.2.0}/src/vclient/models/global_admin.py +0 -0
- {valentina_python_client-1.1.1 → valentina_python_client-1.2.0}/src/vclient/models/pagination.py +0 -0
- {valentina_python_client-1.1.1 → valentina_python_client-1.2.0}/src/vclient/models/shared.py +0 -0
- {valentina_python_client-1.1.1 → valentina_python_client-1.2.0}/src/vclient/models/system.py +0 -0
- {valentina_python_client-1.1.1 → valentina_python_client-1.2.0}/src/vclient/py.typed +0 -0
- {valentina_python_client-1.1.1 → valentina_python_client-1.2.0}/src/vclient/registry.py +0 -0
- {valentina_python_client-1.1.1 → valentina_python_client-1.2.0}/src/vclient/services/__init__.py +0 -0
- {valentina_python_client-1.1.1 → valentina_python_client-1.2.0}/src/vclient/services/base.py +0 -0
- {valentina_python_client-1.1.1 → valentina_python_client-1.2.0}/src/vclient/services/campaign_book_chapters.py +0 -0
- {valentina_python_client-1.1.1 → valentina_python_client-1.2.0}/src/vclient/services/campaign_books.py +0 -0
- {valentina_python_client-1.1.1 → valentina_python_client-1.2.0}/src/vclient/services/campaigns.py +0 -0
- {valentina_python_client-1.1.1 → valentina_python_client-1.2.0}/src/vclient/services/character_autogen.py +0 -0
- {valentina_python_client-1.1.1 → valentina_python_client-1.2.0}/src/vclient/services/characters.py +0 -0
- {valentina_python_client-1.1.1 → valentina_python_client-1.2.0}/src/vclient/services/companies.py +0 -0
- {valentina_python_client-1.1.1 → valentina_python_client-1.2.0}/src/vclient/services/developers.py +0 -0
- {valentina_python_client-1.1.1 → valentina_python_client-1.2.0}/src/vclient/services/dicerolls.py +0 -0
- {valentina_python_client-1.1.1 → valentina_python_client-1.2.0}/src/vclient/services/dictionary.py +0 -0
- {valentina_python_client-1.1.1 → valentina_python_client-1.2.0}/src/vclient/services/global_admin.py +0 -0
- {valentina_python_client-1.1.1 → valentina_python_client-1.2.0}/src/vclient/services/options.py +0 -0
- {valentina_python_client-1.1.1 → valentina_python_client-1.2.0}/src/vclient/services/system.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: valentina-python-client
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.2.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>
|
|
@@ -43,6 +43,28 @@ This client is a supported and up-to-date reference implementation for the Valen
|
|
|
43
43
|
|
|
44
44
|
For complete documentation including configuration options, all available services, response models, and error handling, see the **[Full Documentation](https://docs.valentina-noir.com/python-api-client/)**.
|
|
45
45
|
|
|
46
|
+
## Development Tools
|
|
47
|
+
|
|
48
|
+
### Validate Constants
|
|
49
|
+
|
|
50
|
+
Verify that the `Literal` type constants in this package are in sync with the live API's `/options` endpoint. This catches drift between client and server before a release.
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
# Via duty task
|
|
54
|
+
uv run duty validate_constants
|
|
55
|
+
|
|
56
|
+
# Via script directly
|
|
57
|
+
uv run python scripts/validate_constants.py --api-key <key> --company-id <id>
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
The script reads configuration from (highest precedence first):
|
|
61
|
+
|
|
62
|
+
1. CLI arguments (`--api-url`, `--api-key`, `--company-id`)
|
|
63
|
+
2. System environment variables (`VALENTINA_CLIENT_BASE_URL`, `VALENTINA_CLIENT_API_KEY`, `VALENTINA_CLIENT_DEFAULT_COMPANY_ID`)
|
|
64
|
+
3. A `.env.secrets` file in the project root
|
|
65
|
+
|
|
66
|
+
Exit codes: `0` = all constants match, `1` = mismatches found, `2` = missing configuration.
|
|
67
|
+
|
|
46
68
|
## Resources
|
|
47
69
|
|
|
48
70
|
- [Full Client Documentation](https://docs.valentina-noir.com/python-api-client/)
|
|
@@ -18,6 +18,28 @@ This client is a supported and up-to-date reference implementation for the Valen
|
|
|
18
18
|
|
|
19
19
|
For complete documentation including configuration options, all available services, response models, and error handling, see the **[Full Documentation](https://docs.valentina-noir.com/python-api-client/)**.
|
|
20
20
|
|
|
21
|
+
## Development Tools
|
|
22
|
+
|
|
23
|
+
### Validate Constants
|
|
24
|
+
|
|
25
|
+
Verify that the `Literal` type constants in this package are in sync with the live API's `/options` endpoint. This catches drift between client and server before a release.
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
# Via duty task
|
|
29
|
+
uv run duty validate_constants
|
|
30
|
+
|
|
31
|
+
# Via script directly
|
|
32
|
+
uv run python scripts/validate_constants.py --api-key <key> --company-id <id>
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
The script reads configuration from (highest precedence first):
|
|
36
|
+
|
|
37
|
+
1. CLI arguments (`--api-url`, `--api-key`, `--company-id`)
|
|
38
|
+
2. System environment variables (`VALENTINA_CLIENT_BASE_URL`, `VALENTINA_CLIENT_API_KEY`, `VALENTINA_CLIENT_DEFAULT_COMPANY_ID`)
|
|
39
|
+
3. A `.env.secrets` file in the project root
|
|
40
|
+
|
|
41
|
+
Exit codes: `0` = all constants match, `1` = mismatches found, `2` = missing configuration.
|
|
42
|
+
|
|
21
43
|
## Resources
|
|
22
44
|
|
|
23
45
|
- [Full Client Documentation](https://docs.valentina-noir.com/python-api-client/)
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
name = "valentina-python-client"
|
|
11
11
|
readme = "README.md"
|
|
12
12
|
requires-python = ">=3.13"
|
|
13
|
-
version = "1.
|
|
13
|
+
version = "1.2.0"
|
|
14
14
|
|
|
15
15
|
[project.urls]
|
|
16
16
|
Homepage = "https://docs.valentina-noir.com/python-api-client/"
|
|
@@ -140,8 +140,13 @@
|
|
|
140
140
|
"TD002", # Missing author in TODO
|
|
141
141
|
"TD003", # Missing issue link on the line following this TODO
|
|
142
142
|
]
|
|
143
|
-
per-file-ignores = { "
|
|
143
|
+
per-file-ignores = { "scripts/*.py" = [
|
|
144
|
+
"INP001", # Implicit namespace package (no __init__.py needed)
|
|
145
|
+
"T201", # print() is intentional for CLI output
|
|
146
|
+
], "src/vclient/services/*.py" = [
|
|
144
147
|
"PLR0913", # Too many arguments
|
|
148
|
+
], "src/vclient/validate_constants.py" = [
|
|
149
|
+
"T201", # print() is intentional for report output
|
|
145
150
|
], "tests/**/*.py" = [
|
|
146
151
|
"A002",
|
|
147
152
|
"A003",
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"""Main API client for Valentina."""
|
|
2
2
|
|
|
3
|
+
import os
|
|
3
4
|
from types import TracebackType
|
|
4
5
|
from typing import TYPE_CHECKING, Self
|
|
5
6
|
|
|
@@ -11,6 +12,9 @@ from vclient.constants import (
|
|
|
11
12
|
DEFAULT_MAX_RETRIES,
|
|
12
13
|
DEFAULT_RETRY_DELAY,
|
|
13
14
|
DEFAULT_TIMEOUT,
|
|
15
|
+
ENV_API_KEY,
|
|
16
|
+
ENV_BASE_URL,
|
|
17
|
+
ENV_DEFAULT_COMPANY_ID,
|
|
14
18
|
)
|
|
15
19
|
|
|
16
20
|
if TYPE_CHECKING:
|
|
@@ -64,8 +68,8 @@ class VClient:
|
|
|
64
68
|
|
|
65
69
|
def __init__( # noqa: PLR0913
|
|
66
70
|
self,
|
|
67
|
-
base_url: str,
|
|
68
|
-
api_key: str,
|
|
71
|
+
base_url: str | None = None,
|
|
72
|
+
api_key: str | None = None,
|
|
69
73
|
*,
|
|
70
74
|
timeout: float = DEFAULT_TIMEOUT,
|
|
71
75
|
max_retries: int = DEFAULT_MAX_RETRIES,
|
|
@@ -78,9 +82,18 @@ class VClient:
|
|
|
78
82
|
) -> None:
|
|
79
83
|
"""Initialize the API client.
|
|
80
84
|
|
|
85
|
+
Values for ``base_url``, ``api_key``, and ``default_company_id`` can be
|
|
86
|
+
provided as constructor arguments or via environment variables. Explicit
|
|
87
|
+
arguments always take precedence over environment variables.
|
|
88
|
+
|
|
89
|
+
Environment variables:
|
|
90
|
+
VALENTINA_CLIENT_BASE_URL: Base URL for the API.
|
|
91
|
+
VALENTINA_CLIENT_API_KEY: API key for authentication.
|
|
92
|
+
VALENTINA_CLIENT_DEFAULT_COMPANY_ID: Default company ID.
|
|
93
|
+
|
|
81
94
|
Args:
|
|
82
|
-
base_url: Base URL for the API.
|
|
83
|
-
api_key: API key for authentication.
|
|
95
|
+
base_url: Base URL for the API. Falls back to VALENTINA_CLIENT_BASE_URL.
|
|
96
|
+
api_key: API key for authentication. Falls back to VALENTINA_CLIENT_API_KEY.
|
|
84
97
|
timeout: Request timeout in seconds.
|
|
85
98
|
max_retries: Maximum number of retry attempts for failed requests.
|
|
86
99
|
retry_delay: Base delay between retries in seconds.
|
|
@@ -88,21 +101,38 @@ class VClient:
|
|
|
88
101
|
auto_idempotency_keys: Automatically generate idempotency keys for
|
|
89
102
|
POST/PUT/PATCH requests.
|
|
90
103
|
default_company_id: Default company ID to use when not explicitly provided
|
|
91
|
-
to service factory methods.
|
|
104
|
+
to service factory methods. Falls back to
|
|
105
|
+
VALENTINA_CLIENT_DEFAULT_COMPANY_ID.
|
|
92
106
|
headers: Additional headers to include with all requests.
|
|
93
107
|
set_as_default: If True, register this client as the default for factory
|
|
94
108
|
functions. Set to False when creating multiple clients or when using
|
|
95
109
|
the context manager pattern exclusively.
|
|
110
|
+
|
|
111
|
+
Raises:
|
|
112
|
+
ValueError: If base_url or api_key is not provided and the corresponding
|
|
113
|
+
environment variable is not set.
|
|
96
114
|
"""
|
|
115
|
+
resolved_base_url = base_url or os.environ.get(ENV_BASE_URL)
|
|
116
|
+
if resolved_base_url is None:
|
|
117
|
+
msg = "base_url is required (set it directly or via the VALENTINA_CLIENT_BASE_URL environment variable)"
|
|
118
|
+
raise ValueError(msg)
|
|
119
|
+
|
|
120
|
+
resolved_api_key = api_key or os.environ.get(ENV_API_KEY)
|
|
121
|
+
if resolved_api_key is None:
|
|
122
|
+
msg = "api_key is required (set it directly or via the VALENTINA_CLIENT_API_KEY environment variable)"
|
|
123
|
+
raise ValueError(msg)
|
|
124
|
+
|
|
125
|
+
resolved_company_id = default_company_id or os.environ.get(ENV_DEFAULT_COMPANY_ID)
|
|
126
|
+
|
|
97
127
|
self._config = _APIConfig(
|
|
98
|
-
base_url=
|
|
99
|
-
api_key=
|
|
128
|
+
base_url=resolved_base_url,
|
|
129
|
+
api_key=resolved_api_key,
|
|
100
130
|
timeout=timeout,
|
|
101
131
|
max_retries=max_retries,
|
|
102
132
|
retry_delay=retry_delay,
|
|
103
133
|
auto_retry_rate_limit=auto_retry_rate_limit,
|
|
104
134
|
auto_idempotency_keys=auto_idempotency_keys,
|
|
105
|
-
default_company_id=
|
|
135
|
+
default_company_id=resolved_company_id,
|
|
106
136
|
headers=headers or {},
|
|
107
137
|
)
|
|
108
138
|
|
|
@@ -5,6 +5,10 @@ from typing import Literal
|
|
|
5
5
|
# Authentication
|
|
6
6
|
API_KEY_HEADER = "X-API-KEY"
|
|
7
7
|
|
|
8
|
+
# Environment variable names
|
|
9
|
+
ENV_BASE_URL = "VALENTINA_CLIENT_BASE_URL"
|
|
10
|
+
ENV_API_KEY = "VALENTINA_CLIENT_API_KEY"
|
|
11
|
+
ENV_DEFAULT_COMPANY_ID = "VALENTINA_CLIENT_DEFAULT_COMPANY_ID"
|
|
8
12
|
|
|
9
13
|
# Request defaults
|
|
10
14
|
DEFAULT_TIMEOUT = 30.0
|
|
@@ -124,12 +124,12 @@ class Endpoints:
|
|
|
124
124
|
BLUEPRINT_TRAIT_DETAIL = f"{BLUEPRINT_TRAITS}/{{trait_id}}"
|
|
125
125
|
CONCEPTS = f"{BLUEPRINT_BASE}/concepts"
|
|
126
126
|
CONCEPT_DETAIL = f"{CONCEPTS}/{{concept_id}}"
|
|
127
|
-
VAMPIRE_CLANS = f"{BLUEPRINT_BASE}/
|
|
127
|
+
VAMPIRE_CLANS = f"{BLUEPRINT_BASE}/vampire-clans"
|
|
128
128
|
VAMPIRE_CLAN_DETAIL = f"{VAMPIRE_CLANS}/{{vampire_clan_id}}"
|
|
129
129
|
WEREWOLF_TRIBES = f"{BLUEPRINT_BASE}/werewolf-tribes"
|
|
130
130
|
WEREWOLF_TRIBE_DETAIL = f"{WEREWOLF_TRIBES}/{{werewolf_tribe_id}}"
|
|
131
131
|
WEREWOLF_AUSPICES = f"{BLUEPRINT_BASE}/werewolf-auspices"
|
|
132
|
-
|
|
132
|
+
WEREWOLF_AUSPICE_DETAIL = f"{WEREWOLF_AUSPICES}/{{werewolf_auspice_id}}"
|
|
133
133
|
WEREWOLF_GIFTS = f"{BLUEPRINT_BASE}/werewolf-gifts"
|
|
134
134
|
WEREWOLF_GIFT_DETAIL = f"{WEREWOLF_GIFTS}/{{werewolf_gift_id}}"
|
|
135
135
|
WEREWOLF_RITES = f"{BLUEPRINT_BASE}/werewolf-rites"
|
|
@@ -140,7 +140,7 @@ class Endpoints:
|
|
|
140
140
|
HUNTER_EDGE_PERK_DETAIL = f"{HUNTER_EDGE_PERKS}/{{hunter_edge_perk_id}}"
|
|
141
141
|
|
|
142
142
|
# Dictionary endpoints
|
|
143
|
-
DICTIONARY_TERMS = f"{COMPANY}/
|
|
143
|
+
DICTIONARY_TERMS = f"{COMPANY}/dictionaries"
|
|
144
144
|
DICTIONARY_TERM = f"{DICTIONARY_TERMS}/{{term_id}}"
|
|
145
145
|
|
|
146
146
|
# Dice Rolls
|
|
@@ -436,7 +436,7 @@ class CharacterBlueprintService(BaseService):
|
|
|
436
436
|
"""Get a werewolf auspice by ID."""
|
|
437
437
|
response = await self._get(
|
|
438
438
|
self._format_endpoint(
|
|
439
|
-
Endpoints.
|
|
439
|
+
Endpoints.WEREWOLF_AUSPICE_DETAIL, werewolf_auspice_id=werewolf_auspice_id
|
|
440
440
|
),
|
|
441
441
|
)
|
|
442
442
|
return WerewolfAuspice.model_validate(response.json())
|
|
@@ -251,7 +251,7 @@ class CharacterTraitsService(BaseService):
|
|
|
251
251
|
target_value=new_value,
|
|
252
252
|
currency=currency,
|
|
253
253
|
)
|
|
254
|
-
response = await self.
|
|
254
|
+
response = await self._put(
|
|
255
255
|
self._format_endpoint(
|
|
256
256
|
Endpoints.CHARACTER_TRAIT_VALUE, character_trait_id=character_trait_id
|
|
257
257
|
),
|
{valentina_python_client-1.1.1 → valentina_python_client-1.2.0}/src/vclient/services/users.py
RENAMED
|
@@ -413,6 +413,7 @@ class UsersService(BaseService):
|
|
|
413
413
|
user_id: str,
|
|
414
414
|
campaign_id: str,
|
|
415
415
|
amount: int,
|
|
416
|
+
requesting_user_id: str,
|
|
416
417
|
) -> CampaignExperience:
|
|
417
418
|
"""Award experience points to a user for a specific campaign.
|
|
418
419
|
|
|
@@ -423,6 +424,7 @@ class UsersService(BaseService):
|
|
|
423
424
|
user_id: The ID of the user to award XP to.
|
|
424
425
|
campaign_id: The ID of the campaign to add XP for.
|
|
425
426
|
amount: The amount of XP to add.
|
|
427
|
+
requesting_user_id: ID of the user making the request (for permissions).
|
|
426
428
|
|
|
427
429
|
Returns:
|
|
428
430
|
Updated CampaignExperience object.
|
|
@@ -435,8 +437,8 @@ class UsersService(BaseService):
|
|
|
435
437
|
body = self._validate_request(
|
|
436
438
|
_ExperienceAddRemove,
|
|
437
439
|
amount=amount,
|
|
438
|
-
user_id=user_id,
|
|
439
440
|
campaign_id=campaign_id,
|
|
441
|
+
requesting_user_id=requesting_user_id,
|
|
440
442
|
)
|
|
441
443
|
response = await self._post(
|
|
442
444
|
self._format_endpoint(Endpoints.USER_EXPERIENCE_XP_ADD, user_id=user_id),
|
|
@@ -449,6 +451,7 @@ class UsersService(BaseService):
|
|
|
449
451
|
user_id: str,
|
|
450
452
|
campaign_id: str,
|
|
451
453
|
amount: int,
|
|
454
|
+
requesting_user_id: str,
|
|
452
455
|
) -> CampaignExperience:
|
|
453
456
|
"""Deduct experience points from a user's current XP pool.
|
|
454
457
|
|
|
@@ -458,6 +461,7 @@ class UsersService(BaseService):
|
|
|
458
461
|
user_id: The ID of the user to remove XP from.
|
|
459
462
|
campaign_id: The ID of the campaign to remove XP for.
|
|
460
463
|
amount: The amount of XP to remove.
|
|
464
|
+
requesting_user_id: ID of the user making the request (for permissions).
|
|
461
465
|
|
|
462
466
|
Returns:
|
|
463
467
|
Updated CampaignExperience object.
|
|
@@ -471,8 +475,8 @@ class UsersService(BaseService):
|
|
|
471
475
|
body = self._validate_request(
|
|
472
476
|
_ExperienceAddRemove,
|
|
473
477
|
amount=amount,
|
|
474
|
-
user_id=user_id,
|
|
475
478
|
campaign_id=campaign_id,
|
|
479
|
+
requesting_user_id=requesting_user_id,
|
|
476
480
|
)
|
|
477
481
|
response = await self._post(
|
|
478
482
|
self._format_endpoint(Endpoints.USER_EXPERIENCE_XP_REMOVE, user_id=user_id),
|
|
@@ -485,6 +489,7 @@ class UsersService(BaseService):
|
|
|
485
489
|
user_id: str,
|
|
486
490
|
campaign_id: str,
|
|
487
491
|
amount: int,
|
|
492
|
+
requesting_user_id: str,
|
|
488
493
|
) -> CampaignExperience:
|
|
489
494
|
"""Award cool points to a user for a specific campaign.
|
|
490
495
|
|
|
@@ -495,6 +500,7 @@ class UsersService(BaseService):
|
|
|
495
500
|
user_id: The ID of the user to award cool points to.
|
|
496
501
|
campaign_id: The ID of the campaign to add cool points for.
|
|
497
502
|
amount: The amount of cool points to add.
|
|
503
|
+
requesting_user_id: ID of the user making the request (for permissions).
|
|
498
504
|
|
|
499
505
|
Returns:
|
|
500
506
|
Updated CampaignExperience object.
|
|
@@ -507,8 +513,8 @@ class UsersService(BaseService):
|
|
|
507
513
|
body = self._validate_request(
|
|
508
514
|
_ExperienceAddRemove,
|
|
509
515
|
amount=amount,
|
|
510
|
-
user_id=user_id,
|
|
511
516
|
campaign_id=campaign_id,
|
|
517
|
+
requesting_user_id=requesting_user_id,
|
|
512
518
|
)
|
|
513
519
|
response = await self._post(
|
|
514
520
|
self._format_endpoint(Endpoints.USER_EXPERIENCE_CP_ADD, user_id=user_id),
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
"""Validate client constants against the API options endpoint.
|
|
2
|
+
|
|
3
|
+
Compare the Literal type constants defined in constants.py against the
|
|
4
|
+
values returned by the API's /options endpoint to detect drift between
|
|
5
|
+
client and server.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import typing
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
|
|
13
|
+
from vclient import constants
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(frozen=True)
|
|
17
|
+
class ConstantMapping:
|
|
18
|
+
"""Map a local constant name to its location in the API options response.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
api_category: Top-level key in the options response (e.g., "characters").
|
|
22
|
+
api_option: Option name within that category (e.g., "CharacterClass").
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
api_category: str
|
|
26
|
+
api_option: str
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass
|
|
30
|
+
class ConstantMismatch:
|
|
31
|
+
"""Record of a single constant that differs between client and API.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
constant_name: The local constant name in constants.py.
|
|
35
|
+
api_category: The API category this constant maps to.
|
|
36
|
+
api_option: The API option name this constant maps to.
|
|
37
|
+
missing_from_client: Values present in the API but not in the local Literal.
|
|
38
|
+
extra_in_client: Values present in the local Literal but not in the API.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
constant_name: str
|
|
42
|
+
api_category: str
|
|
43
|
+
api_option: str
|
|
44
|
+
missing_from_client: set[str | int] = field(default_factory=set)
|
|
45
|
+
extra_in_client: set[str | int] = field(default_factory=set)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass
|
|
49
|
+
class ValidationResult:
|
|
50
|
+
"""Result of validating client constants against the API.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
is_valid: True if all mapped constants match and no unmapped API options exist.
|
|
54
|
+
mismatches: Constants with value differences between client and API.
|
|
55
|
+
unmapped_api_options: API option keys that have no corresponding local constant.
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
is_valid: bool
|
|
59
|
+
mismatches: list[ConstantMismatch] = field(default_factory=list)
|
|
60
|
+
unmapped_api_options: dict[str, list[str]] = field(default_factory=dict)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
CONSTANT_MAP: dict[str, ConstantMapping] = {
|
|
64
|
+
"AbilityFocus": ConstantMapping("characters", "AbilityFocus"),
|
|
65
|
+
"AutoGenExperienceLevel": ConstantMapping("characters", "AutoGenExperienceLevel"),
|
|
66
|
+
"BlueprintTraitOrderBy": ConstantMapping("characters", "BlueprintTraitOrderBy"),
|
|
67
|
+
"CharacterClass": ConstantMapping("characters", "CharacterClass"),
|
|
68
|
+
"CharacterInventoryType": ConstantMapping("characters", "InventoryItemType"),
|
|
69
|
+
"CharacterStatus": ConstantMapping("characters", "CharacterStatus"),
|
|
70
|
+
"CharacterType": ConstantMapping("characters", "CharacterType"),
|
|
71
|
+
"DiceSize": ConstantMapping("gameplay", "DiceSize"),
|
|
72
|
+
"FreeTraitChangesPermission": ConstantMapping("companies", "PermissionsFreeTraitChanges"),
|
|
73
|
+
"GameVersion": ConstantMapping("characters", "GameVersion"),
|
|
74
|
+
"GrantXPPermission": ConstantMapping("companies", "PermissionsGrantXP"),
|
|
75
|
+
"HunterCreed": ConstantMapping("characters", "HunterCreed"),
|
|
76
|
+
"HunterEdgeType": ConstantMapping("characters", "HunterEdgeType"),
|
|
77
|
+
"ManageCampaignPermission": ConstantMapping("companies", "PermissionManageCampaign"),
|
|
78
|
+
"PermissionLevel": ConstantMapping("companies", "CompanyPermission"),
|
|
79
|
+
"RollResultType": ConstantMapping("gameplay", "RollResultType"),
|
|
80
|
+
"S3AssetParentType": ConstantMapping("assets", "AssetParentType"),
|
|
81
|
+
"S3AssetType": ConstantMapping("assets", "AssetType"),
|
|
82
|
+
"SpecialtyType": ConstantMapping("characters", "SpecialtyType"),
|
|
83
|
+
"TraitModifyCurrency": ConstantMapping("characters", "TraitModifyCurrency"),
|
|
84
|
+
"UserRole": ConstantMapping("users", "UserRole"),
|
|
85
|
+
"WerewolfRenown": ConstantMapping("characters", "WerewolfRenown"),
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def validate(api_options: dict[str, dict[str, list | dict]]) -> ValidationResult:
|
|
90
|
+
"""Compare local Literal constants against values from the API options endpoint.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
api_options: The raw dictionary returned by OptionsService.get_options().
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
ValidationResult with is_valid=True if all constants match, otherwise
|
|
97
|
+
populated with mismatches and unmapped API options.
|
|
98
|
+
"""
|
|
99
|
+
mismatches: list[ConstantMismatch] = []
|
|
100
|
+
mapped_api_options: set[tuple[str, str]] = set()
|
|
101
|
+
|
|
102
|
+
for constant_name, mapping in CONSTANT_MAP.items():
|
|
103
|
+
mapped_api_options.add((mapping.api_category, mapping.api_option))
|
|
104
|
+
|
|
105
|
+
local_values = set(typing.get_args(getattr(constants, constant_name)))
|
|
106
|
+
|
|
107
|
+
category_data = api_options.get(mapping.api_category, {})
|
|
108
|
+
api_values_raw = category_data.get(mapping.api_option)
|
|
109
|
+
if api_values_raw is None:
|
|
110
|
+
mismatches.append(
|
|
111
|
+
ConstantMismatch(
|
|
112
|
+
constant_name=constant_name,
|
|
113
|
+
api_category=mapping.api_category,
|
|
114
|
+
api_option=mapping.api_option,
|
|
115
|
+
missing_from_client=set(),
|
|
116
|
+
extra_in_client=local_values,
|
|
117
|
+
)
|
|
118
|
+
)
|
|
119
|
+
continue
|
|
120
|
+
|
|
121
|
+
api_values = set(api_values_raw)
|
|
122
|
+
missing_from_client = api_values - local_values
|
|
123
|
+
extra_in_client = local_values - api_values
|
|
124
|
+
|
|
125
|
+
if missing_from_client or extra_in_client:
|
|
126
|
+
mismatches.append(
|
|
127
|
+
ConstantMismatch(
|
|
128
|
+
constant_name=constant_name,
|
|
129
|
+
api_category=mapping.api_category,
|
|
130
|
+
api_option=mapping.api_option,
|
|
131
|
+
missing_from_client=missing_from_client,
|
|
132
|
+
extra_in_client=extra_in_client,
|
|
133
|
+
)
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
unmapped_api_options: dict[str, list[str]] = {}
|
|
137
|
+
for category, options in api_options.items():
|
|
138
|
+
if not isinstance(options, dict):
|
|
139
|
+
continue
|
|
140
|
+
for option_name, option_values in options.items():
|
|
141
|
+
if option_name.startswith("_"):
|
|
142
|
+
continue
|
|
143
|
+
if not isinstance(option_values, list):
|
|
144
|
+
continue
|
|
145
|
+
if (category, option_name) not in mapped_api_options:
|
|
146
|
+
unmapped_api_options.setdefault(category, []).append(option_name)
|
|
147
|
+
|
|
148
|
+
is_valid = len(mismatches) == 0 and len(unmapped_api_options) == 0
|
|
149
|
+
|
|
150
|
+
return ValidationResult(
|
|
151
|
+
is_valid=is_valid,
|
|
152
|
+
mismatches=mismatches,
|
|
153
|
+
unmapped_api_options=unmapped_api_options,
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _print_status_lines(mismatched_names: set[str]) -> None:
|
|
158
|
+
"""Print OK/FAIL status line for each constant in the mapping table."""
|
|
159
|
+
for constant_name in sorted(CONSTANT_MAP):
|
|
160
|
+
status = "FAIL" if constant_name in mismatched_names else "OK "
|
|
161
|
+
print(f" {status} {constant_name}")
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _print_mismatches(mismatches: list[ConstantMismatch]) -> None:
|
|
165
|
+
"""Print detailed mismatch information for each failing constant."""
|
|
166
|
+
print()
|
|
167
|
+
print("-" * 60)
|
|
168
|
+
print("Mismatches:")
|
|
169
|
+
print("-" * 60)
|
|
170
|
+
for mismatch in mismatches:
|
|
171
|
+
print(f"\n {mismatch.constant_name}")
|
|
172
|
+
print(f" API: {mismatch.api_category}.{mismatch.api_option}")
|
|
173
|
+
if mismatch.missing_from_client:
|
|
174
|
+
print(f" Missing from client: {sorted(mismatch.missing_from_client)}")
|
|
175
|
+
if mismatch.extra_in_client:
|
|
176
|
+
print(f" Extra in client: {sorted(mismatch.extra_in_client)}")
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _print_unmapped(unmapped_api_options: dict[str, list[str]]) -> None:
|
|
180
|
+
"""Print API options that have no corresponding local constant."""
|
|
181
|
+
print()
|
|
182
|
+
print("-" * 60)
|
|
183
|
+
print("Unmapped API options (no local constant):")
|
|
184
|
+
print("-" * 60)
|
|
185
|
+
for category, options in sorted(unmapped_api_options.items()):
|
|
186
|
+
for option in sorted(options):
|
|
187
|
+
print(f" {category}.{option}")
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _build_summary(
|
|
191
|
+
matched_count: int,
|
|
192
|
+
total: int,
|
|
193
|
+
mismatch_count: int,
|
|
194
|
+
unmapped_api_options: dict[str, list[str]],
|
|
195
|
+
) -> str:
|
|
196
|
+
"""Build the final summary line for the report."""
|
|
197
|
+
if mismatch_count == 0 and not unmapped_api_options:
|
|
198
|
+
return f" {matched_count}/{total} constants in sync"
|
|
199
|
+
|
|
200
|
+
summary_parts: list[str] = []
|
|
201
|
+
if mismatch_count:
|
|
202
|
+
summary_parts.append(f"{mismatch_count} mismatch(es)")
|
|
203
|
+
if unmapped_api_options:
|
|
204
|
+
unmapped_count = sum(len(v) for v in unmapped_api_options.values())
|
|
205
|
+
summary_parts.append(f"{unmapped_count} unmapped API option(s)")
|
|
206
|
+
return f" {matched_count}/{total} constants in sync, {', '.join(summary_parts)}"
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def print_report(result: ValidationResult) -> None:
|
|
210
|
+
"""Print a human-readable validation report to stdout.
|
|
211
|
+
|
|
212
|
+
Args:
|
|
213
|
+
result: The ValidationResult from validate().
|
|
214
|
+
"""
|
|
215
|
+
total = len(CONSTANT_MAP)
|
|
216
|
+
mismatch_count = len(result.mismatches)
|
|
217
|
+
matched_count = total - mismatch_count
|
|
218
|
+
mismatched_names = {m.constant_name for m in result.mismatches}
|
|
219
|
+
|
|
220
|
+
print()
|
|
221
|
+
print("=" * 60)
|
|
222
|
+
print("Constants Validation Report")
|
|
223
|
+
print("=" * 60)
|
|
224
|
+
|
|
225
|
+
_print_status_lines(mismatched_names)
|
|
226
|
+
|
|
227
|
+
if result.mismatches:
|
|
228
|
+
_print_mismatches(result.mismatches)
|
|
229
|
+
|
|
230
|
+
if result.unmapped_api_options:
|
|
231
|
+
_print_unmapped(result.unmapped_api_options)
|
|
232
|
+
|
|
233
|
+
print()
|
|
234
|
+
print("=" * 60)
|
|
235
|
+
print(_build_summary(matched_count, total, mismatch_count, result.unmapped_api_options))
|
|
236
|
+
print("=" * 60)
|
|
237
|
+
print()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{valentina_python_client-1.1.1 → valentina_python_client-1.2.0}/src/vclient/models/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{valentina_python_client-1.1.1 → valentina_python_client-1.2.0}/src/vclient/models/campaigns.py
RENAMED
|
File without changes
|
{valentina_python_client-1.1.1 → valentina_python_client-1.2.0}/src/vclient/models/chapters.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{valentina_python_client-1.1.1 → valentina_python_client-1.2.0}/src/vclient/models/characters.py
RENAMED
|
File without changes
|
{valentina_python_client-1.1.1 → valentina_python_client-1.2.0}/src/vclient/models/companies.py
RENAMED
|
File without changes
|
{valentina_python_client-1.1.1 → valentina_python_client-1.2.0}/src/vclient/models/developers.py
RENAMED
|
File without changes
|
{valentina_python_client-1.1.1 → valentina_python_client-1.2.0}/src/vclient/models/diceroll.py
RENAMED
|
File without changes
|
{valentina_python_client-1.1.1 → valentina_python_client-1.2.0}/src/vclient/models/dictionary.py
RENAMED
|
File without changes
|
{valentina_python_client-1.1.1 → valentina_python_client-1.2.0}/src/vclient/models/global_admin.py
RENAMED
|
File without changes
|
{valentina_python_client-1.1.1 → valentina_python_client-1.2.0}/src/vclient/models/pagination.py
RENAMED
|
File without changes
|
{valentina_python_client-1.1.1 → valentina_python_client-1.2.0}/src/vclient/models/shared.py
RENAMED
|
File without changes
|
{valentina_python_client-1.1.1 → valentina_python_client-1.2.0}/src/vclient/models/system.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{valentina_python_client-1.1.1 → valentina_python_client-1.2.0}/src/vclient/services/__init__.py
RENAMED
|
File without changes
|
{valentina_python_client-1.1.1 → valentina_python_client-1.2.0}/src/vclient/services/base.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{valentina_python_client-1.1.1 → valentina_python_client-1.2.0}/src/vclient/services/campaigns.py
RENAMED
|
File without changes
|
|
File without changes
|
{valentina_python_client-1.1.1 → valentina_python_client-1.2.0}/src/vclient/services/characters.py
RENAMED
|
File without changes
|
{valentina_python_client-1.1.1 → valentina_python_client-1.2.0}/src/vclient/services/companies.py
RENAMED
|
File without changes
|
{valentina_python_client-1.1.1 → valentina_python_client-1.2.0}/src/vclient/services/developers.py
RENAMED
|
File without changes
|
{valentina_python_client-1.1.1 → valentina_python_client-1.2.0}/src/vclient/services/dicerolls.py
RENAMED
|
File without changes
|
{valentina_python_client-1.1.1 → valentina_python_client-1.2.0}/src/vclient/services/dictionary.py
RENAMED
|
File without changes
|
{valentina_python_client-1.1.1 → valentina_python_client-1.2.0}/src/vclient/services/global_admin.py
RENAMED
|
File without changes
|
{valentina_python_client-1.1.1 → valentina_python_client-1.2.0}/src/vclient/services/options.py
RENAMED
|
File without changes
|
{valentina_python_client-1.1.1 → valentina_python_client-1.2.0}/src/vclient/services/system.py
RENAMED
|
File without changes
|