sg-python-api 0.1.2__py3-none-any.whl → 0.2.0__py3-none-any.whl
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.
- {sg_python_api-0.1.2.dist-info → sg_python_api-0.2.0.dist-info}/METADATA +21 -24
- sg_python_api-0.2.0.dist-info/RECORD +58 -0
- shotgun_api3/__init__.py +4 -0
- shotgun_api3/_async_client.py +347 -281
- shotgun_api3/_async_driver.py +25 -55
- shotgun_api3/_async_httpx2.py +92 -59
- shotgun_api3/_async_transport.py +38 -3
- shotgun_api3/_capabilities.py +4 -3
- shotgun_api3/_client.py +296 -422
- shotgun_api3/_config.py +245 -120
- shotgun_api3/_connections.py +144 -0
- shotgun_api3/_crud.py +25 -250
- shotgun_api3/_driver.py +29 -54
- shotgun_api3/_execution.py +1 -3
- shotgun_api3/_files.py +193 -21
- shotgun_api3/_methods.py +59 -411
- shotgun_api3/_protocol.py +27 -53
- shotgun_api3/_transport.py +82 -407
- shotgun_api3/_version.py +7 -2
- shotgun_api3/lib/mockgun/__init__.py +20 -7
- shotgun_api3/lib/mockgun/_activity.py +510 -0
- shotgun_api3/lib/mockgun/_async_client.py +196 -0
- shotgun_api3/lib/mockgun/_dates.py +164 -0
- shotgun_api3/lib/mockgun/_errors.py +18 -0
- shotgun_api3/lib/mockgun/_events.py +1031 -0
- shotgun_api3/lib/mockgun/_follows.py +217 -0
- shotgun_api3/lib/mockgun/_handlers.py +558 -0
- shotgun_api3/lib/mockgun/_links.py +440 -0
- shotgun_api3/lib/mockgun/_new_client.py +92 -0
- shotgun_api3/lib/mockgun/_presets.py +492 -0
- shotgun_api3/lib/mockgun/_query.py +2163 -0
- shotgun_api3/lib/mockgun/_retirement.py +368 -0
- shotgun_api3/lib/mockgun/_schema.py +172 -0
- shotgun_api3/lib/mockgun/_schema_writes.py +425 -0
- shotgun_api3/lib/mockgun/_search.py +378 -0
- shotgun_api3/lib/mockgun/_server.py +169 -0
- shotgun_api3/lib/mockgun/_sessions.py +206 -0
- shotgun_api3/lib/mockgun/_site.py +150 -0
- shotgun_api3/lib/mockgun/_site_data.py +584 -0
- shotgun_api3/lib/mockgun/_storage.py +208 -0
- shotgun_api3/lib/mockgun/_store.py +1446 -0
- shotgun_api3/lib/mockgun/_summaries.py +964 -0
- shotgun_api3/lib/mockgun/_sync_client.py +190 -0
- shotgun_api3/lib/mockgun/_threads.py +226 -0
- shotgun_api3/lib/mockgun/_thumbnails.py +146 -0
- shotgun_api3/lib/mockgun/_transport.py +145 -0
- shotgun_api3/lib/mockgun/_uploads.py +417 -0
- shotgun_api3/lib/mockgun/_wire.py +120 -0
- shotgun_api3/lib/mockgun/mockgun.py +14 -0
- shotgun_api3/lib/sgtimezone.py +24 -1
- sg_python_api-0.1.2.dist-info/RECORD +0 -31
- shotgun_api3/_signatures.py +0 -53
- shotgun_api3/lib/mockgun/_async.py +0 -315
- shotgun_api3/lib/mockgun/_mockgun.py +0 -1666
- {sg_python_api-0.1.2.dist-info → sg_python_api-0.2.0.dist-info}/WHEEL +0 -0
- {sg_python_api-0.1.2.dist-info → sg_python_api-0.2.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
"""Run the real asynchronous client against an isolated in-memory site."""
|
|
2
|
+
|
|
3
|
+
import dataclasses
|
|
4
|
+
from typing import ClassVar, Self
|
|
5
|
+
|
|
6
|
+
from shotgun_api3 import _crud
|
|
7
|
+
from shotgun_api3._async_client import AsyncShotgun as RealAsyncShotgun
|
|
8
|
+
from shotgun_api3._async_transport import AsyncTransport
|
|
9
|
+
from shotgun_api3._client import ConstructorOptions
|
|
10
|
+
from shotgun_api3._config import RequestContext
|
|
11
|
+
|
|
12
|
+
from ._new_client import (
|
|
13
|
+
SHARED_SERVER,
|
|
14
|
+
copy_client_settings,
|
|
15
|
+
default_credentials,
|
|
16
|
+
new_client_options,
|
|
17
|
+
)
|
|
18
|
+
from ._schema import load_schema
|
|
19
|
+
from ._server import FakeServer
|
|
20
|
+
from ._site import Site
|
|
21
|
+
from ._sync_client import Shotgun
|
|
22
|
+
from ._transport import AsyncInMemoryTransport
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class AsyncShotgun(RealAsyncShotgun):
|
|
26
|
+
"""A real client whose transport reaches only its own in-memory fake site.
|
|
27
|
+
|
|
28
|
+
Configure schema paths before construction. Direct construction creates
|
|
29
|
+
independent site state; `new_client()` shares the site with new
|
|
30
|
+
credentials. Test controls are exposed through `site`.
|
|
31
|
+
|
|
32
|
+
Attributes:
|
|
33
|
+
finds: Number of find operations performed.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
_in_process: ClassVar[bool] = True
|
|
37
|
+
_schema_paths: ClassVar[tuple[str, str] | None] = None
|
|
38
|
+
|
|
39
|
+
def __init__(
|
|
40
|
+
self,
|
|
41
|
+
base_url: str,
|
|
42
|
+
script_name: str | None = None,
|
|
43
|
+
api_key: str | None = None,
|
|
44
|
+
convert_datetimes_to_utc: bool = True,
|
|
45
|
+
http_proxy: str | None = None,
|
|
46
|
+
connect: bool = True,
|
|
47
|
+
ca_certs: str | None = None,
|
|
48
|
+
login: str | None = None,
|
|
49
|
+
password: str | None = None,
|
|
50
|
+
sudo_as_login: str | None = None,
|
|
51
|
+
session_token: str | None = None,
|
|
52
|
+
auth_token: str | None = None,
|
|
53
|
+
) -> None:
|
|
54
|
+
"""Initialize a fake site before the real client's readiness operation.
|
|
55
|
+
|
|
56
|
+
Load the configured schema files synchronously for a new fake site.
|
|
57
|
+
Readiness and session operations remain deferred to the async client's
|
|
58
|
+
normal entry points.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
base_url: Flow Production Tracking site URL.
|
|
62
|
+
script_name: Script name for script authentication.
|
|
63
|
+
api_key: API key for script authentication.
|
|
64
|
+
convert_datetimes_to_utc: Whether to convert datetime values to
|
|
65
|
+
UTC.
|
|
66
|
+
http_proxy: Optional proxy authority in `[user:pass@]host[:port]`
|
|
67
|
+
form.
|
|
68
|
+
connect: Whether to request eager readiness during asynchronous
|
|
69
|
+
context entry.
|
|
70
|
+
ca_certs: Optional certificate-authority bundle path.
|
|
71
|
+
login: Login for human-user authentication.
|
|
72
|
+
password: Password for human-user authentication.
|
|
73
|
+
sudo_as_login: Optional user login for sudo impersonation.
|
|
74
|
+
session_token: Existing session token for authentication.
|
|
75
|
+
auth_token: Optional two-factor authentication token.
|
|
76
|
+
|
|
77
|
+
Raises:
|
|
78
|
+
MockgunError: If schema paths are unset or files are missing.
|
|
79
|
+
OSError: If a schema file cannot be opened or read.
|
|
80
|
+
EOFError: If a schema pickle ends prematurely.
|
|
81
|
+
pickle.UnpicklingError: If a schema pickle is invalid.
|
|
82
|
+
ValueError: If a pickle protocol is unsupported, constructor values
|
|
83
|
+
fail real-client validation, or the retry environment setting
|
|
84
|
+
is invalid.
|
|
85
|
+
"""
|
|
86
|
+
options = ConstructorOptions(
|
|
87
|
+
base_url,
|
|
88
|
+
script_name,
|
|
89
|
+
api_key,
|
|
90
|
+
convert_datetimes_to_utc,
|
|
91
|
+
http_proxy,
|
|
92
|
+
connect,
|
|
93
|
+
ca_certs,
|
|
94
|
+
login,
|
|
95
|
+
password,
|
|
96
|
+
sudo_as_login,
|
|
97
|
+
session_token,
|
|
98
|
+
auth_token,
|
|
99
|
+
)
|
|
100
|
+
shared = SHARED_SERVER.get()
|
|
101
|
+
if shared is None:
|
|
102
|
+
options = default_credentials(options)
|
|
103
|
+
self._server = (
|
|
104
|
+
shared
|
|
105
|
+
if shared is not None
|
|
106
|
+
else FakeServer(load_schema(*self._configured_schema_paths()))
|
|
107
|
+
)
|
|
108
|
+
self._options = options
|
|
109
|
+
self._site = Site(self._server)
|
|
110
|
+
if options.login is not None:
|
|
111
|
+
self._server.admit_login(options.login)
|
|
112
|
+
self.finds = 0
|
|
113
|
+
super().__init__(
|
|
114
|
+
options.base_url,
|
|
115
|
+
options.script_name,
|
|
116
|
+
options.api_key,
|
|
117
|
+
options.convert_datetimes_to_utc,
|
|
118
|
+
options.http_proxy,
|
|
119
|
+
options.connect,
|
|
120
|
+
options.ca_certs,
|
|
121
|
+
options.login,
|
|
122
|
+
options.password,
|
|
123
|
+
options.sudo_as_login,
|
|
124
|
+
options.session_token,
|
|
125
|
+
options.auth_token,
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
@classmethod
|
|
129
|
+
def _configured_schema_paths(cls) -> tuple[str | None, str | None]:
|
|
130
|
+
"""Resolve subclass paths or the base synchronous client's defaults."""
|
|
131
|
+
return cls._schema_paths if cls._schema_paths is not None else Shotgun.get_schema_paths()
|
|
132
|
+
|
|
133
|
+
async def _find(self, options: _crud.FindOptions) -> list[dict[str, object]]:
|
|
134
|
+
"""Count each query before dispatching through the real client."""
|
|
135
|
+
self.finds += 1
|
|
136
|
+
return await super()._find(options)
|
|
137
|
+
|
|
138
|
+
def _new_transport(self, context: RequestContext) -> AsyncTransport:
|
|
139
|
+
"""Bind every operation to this client's in-memory site."""
|
|
140
|
+
del context
|
|
141
|
+
return AsyncInMemoryTransport(self._server)
|
|
142
|
+
|
|
143
|
+
@classmethod
|
|
144
|
+
async def get_schema_paths(cls) -> tuple[str | None, str | None]:
|
|
145
|
+
"""Return field-schema and entity-schema paths, in that order."""
|
|
146
|
+
return cls._configured_schema_paths()
|
|
147
|
+
|
|
148
|
+
def new_client(self, **credentials: object) -> Self:
|
|
149
|
+
"""Create a client with new credentials sharing this client's fake site.
|
|
150
|
+
|
|
151
|
+
This is a plain method on both sync and async clients. The returned
|
|
152
|
+
client has its own readiness and session state. Timeout, retry and
|
|
153
|
+
user-agent settings are copied; other noncredential options use their
|
|
154
|
+
original constructor values.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
**credentials: Constructor credential arguments. A lone
|
|
158
|
+
`sudo_as_login` preserves the original credentials; otherwise
|
|
159
|
+
only the supplied credentials are used.
|
|
160
|
+
|
|
161
|
+
Returns:
|
|
162
|
+
A new instance of this client's class sharing schema and records.
|
|
163
|
+
|
|
164
|
+
Raises:
|
|
165
|
+
ValueError: If constructor credentials, retained configuration, or
|
|
166
|
+
the retry environment setting fail validation.
|
|
167
|
+
"""
|
|
168
|
+
options = new_client_options(self._options, credentials)
|
|
169
|
+
token = SHARED_SERVER.set(self._server)
|
|
170
|
+
try:
|
|
171
|
+
client = type(self)(**dataclasses.asdict(options))
|
|
172
|
+
finally:
|
|
173
|
+
SHARED_SERVER.reset(token)
|
|
174
|
+
copy_client_settings(self.config, client.config)
|
|
175
|
+
return client
|
|
176
|
+
|
|
177
|
+
@classmethod
|
|
178
|
+
async def set_schema_paths(cls, schema_path: str, schema_entity_path: str) -> None:
|
|
179
|
+
"""Configure the trusted schema pickles loaded by future instances.
|
|
180
|
+
|
|
181
|
+
On `AsyncShotgun` itself, these paths are shared with
|
|
182
|
+
`mockgun.Shotgun`.
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
schema_path: Path to the field-schema pickle.
|
|
186
|
+
schema_entity_path: Path to the entity-schema pickle.
|
|
187
|
+
"""
|
|
188
|
+
if cls is AsyncShotgun:
|
|
189
|
+
Shotgun.set_schema_paths(schema_path, schema_entity_path)
|
|
190
|
+
else:
|
|
191
|
+
cls._schema_paths = (schema_path, schema_entity_path)
|
|
192
|
+
|
|
193
|
+
@property
|
|
194
|
+
def site(self) -> Site:
|
|
195
|
+
"""The test-control facade for this client's in-memory site."""
|
|
196
|
+
return self._site
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"""Validate and evaluate settled relative-date windows in request time."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Sequence
|
|
4
|
+
from datetime import datetime, timedelta
|
|
5
|
+
from typing import cast
|
|
6
|
+
|
|
7
|
+
from ._errors import MockgunError, ServerFaultError
|
|
8
|
+
from ._sessions import RequestContext
|
|
9
|
+
from ._store import FieldValue
|
|
10
|
+
|
|
11
|
+
_ROLLING_ARITY = 2
|
|
12
|
+
_UNITS = ('HOUR', 'DAY', 'WEEK', 'MONTH', 'YEAR')
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _shift(now: datetime, amount: int, unit: str) -> datetime:
|
|
16
|
+
"""Shift by elapsed time or by the same calendar day without clamping."""
|
|
17
|
+
if unit == 'MONTH':
|
|
18
|
+
year, month = divmod(now.year * 12 + now.month - 1 + amount, 12)
|
|
19
|
+
return now.replace(year=year, month=month + 1)
|
|
20
|
+
if unit == 'YEAR':
|
|
21
|
+
return now.replace(year=now.year + amount)
|
|
22
|
+
return now + timedelta(hours=amount * {'HOUR': 1, 'DAY': 24, 'WEEK': 168}[unit])
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _valid_operands(field_type: str, relation: str, values: Sequence[object], prefix: str) -> bool:
|
|
26
|
+
"""Distinguish characterized validation faults from unsettled operand forms."""
|
|
27
|
+
calendar = relation.startswith('in_calendar_')
|
|
28
|
+
if field_type == 'date_time' and relation == 'in_last':
|
|
29
|
+
if len(values) != _ROLLING_ARITY:
|
|
30
|
+
raise ServerFaultError(f'{prefix} expects [amount, unit]', error_code=103)
|
|
31
|
+
amount, unit = values
|
|
32
|
+
if type(amount) is not int:
|
|
33
|
+
raise ServerFaultError(f'{prefix} requires an Integer amount', error_code=103)
|
|
34
|
+
if amount < 0:
|
|
35
|
+
raise ServerFaultError(f'{prefix} requires a positive Integer amount', error_code=103)
|
|
36
|
+
if unit not in _UNITS:
|
|
37
|
+
raise ServerFaultError(
|
|
38
|
+
f'{prefix} unit {unit} is not supported. Valid units: {", ".join(_UNITS)}',
|
|
39
|
+
error_code=103,
|
|
40
|
+
)
|
|
41
|
+
valid = amount > 0 or unit == 'DAY'
|
|
42
|
+
elif field_type == 'date_time' and relation == 'in_calendar_day':
|
|
43
|
+
if len(values) != 1 or type(values[0]) is not int:
|
|
44
|
+
raise ServerFaultError(f'{prefix} requires an Integer offset', error_code=103)
|
|
45
|
+
valid = True
|
|
46
|
+
elif calendar:
|
|
47
|
+
valid = len(values) == 1 and type(values[0]) is int
|
|
48
|
+
else:
|
|
49
|
+
valid = (
|
|
50
|
+
len(values) == _ROLLING_ARITY
|
|
51
|
+
and type(values[0]) is int
|
|
52
|
+
and values[0] > 0
|
|
53
|
+
and values[1] in _UNITS
|
|
54
|
+
)
|
|
55
|
+
return valid
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _window(
|
|
59
|
+
field_type: str, relation: str, values: Sequence[object], now: datetime
|
|
60
|
+
) -> tuple[datetime, datetime]:
|
|
61
|
+
"""Build rolling or calendar bounds after operand validation."""
|
|
62
|
+
amount = cast('int', values[0])
|
|
63
|
+
if relation.startswith('in_calendar_'):
|
|
64
|
+
unit = relation.removeprefix('in_calendar_').upper()
|
|
65
|
+
start = now.replace(hour=0, minute=0, second=0, microsecond=0)
|
|
66
|
+
if unit == 'WEEK':
|
|
67
|
+
start -= timedelta(days=(start.weekday() + 1) % 7)
|
|
68
|
+
elif unit == 'MONTH':
|
|
69
|
+
start = start.replace(day=1)
|
|
70
|
+
elif unit == 'YEAR':
|
|
71
|
+
start = start.replace(month=1, day=1)
|
|
72
|
+
lower = amount if amount < 0 else (1 if amount > 0 else 0)
|
|
73
|
+
upper = 0 if amount < 0 else amount + 1
|
|
74
|
+
return _shift(start, lower, unit), _shift(start, upper, unit)
|
|
75
|
+
unit = cast('str', values[1])
|
|
76
|
+
if field_type == 'date':
|
|
77
|
+
now = now.replace(hour=0, minute=0, second=0, microsecond=0)
|
|
78
|
+
if unit == 'HOUR':
|
|
79
|
+
amount //= 24
|
|
80
|
+
unit = 'DAY'
|
|
81
|
+
if relation.endswith('last'):
|
|
82
|
+
return _shift(now, -amount, unit), now
|
|
83
|
+
return now, _shift(now, amount, unit)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def in_window(
|
|
87
|
+
field_type: str, relation: str, values: Sequence[object], value: FieldValue, *, now: datetime
|
|
88
|
+
) -> bool:
|
|
89
|
+
"""Match a validated relative filter, rejecting unsettled datetime endpoints.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
field_type: Either `date` or `date_time`.
|
|
93
|
+
relation: Validated rolling or calendar relation.
|
|
94
|
+
values: Validated amount and unit, or calendar offset.
|
|
95
|
+
value: Stored date string, datetime, or null.
|
|
96
|
+
now: Aware UTC request timestamp.
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
Whether the stored value matches the relation. A null value matches
|
|
100
|
+
only `not_in_last` and `not_in_next`.
|
|
101
|
+
|
|
102
|
+
Raises:
|
|
103
|
+
MockgunError: A stored date cannot be parsed or a datetime equals a
|
|
104
|
+
rolling endpoint.
|
|
105
|
+
"""
|
|
106
|
+
negative = relation in {'not_in_last', 'not_in_next'}
|
|
107
|
+
if value is None:
|
|
108
|
+
return negative
|
|
109
|
+
lower, upper = _window(field_type, relation, values, now)
|
|
110
|
+
if field_type == 'date':
|
|
111
|
+
try:
|
|
112
|
+
actual_date = datetime.fromisoformat(cast('str', value)).date()
|
|
113
|
+
except ValueError as exc:
|
|
114
|
+
raise MockgunError(f'stored date cannot be parsed: {value!r}') from exc
|
|
115
|
+
matches = lower.date() <= actual_date <= upper.date()
|
|
116
|
+
if relation.startswith('in_calendar_'):
|
|
117
|
+
matches = lower.date() <= actual_date < upper.date()
|
|
118
|
+
else:
|
|
119
|
+
actual = cast('datetime', value)
|
|
120
|
+
if relation.startswith('in_calendar_'):
|
|
121
|
+
matches = lower <= actual < upper
|
|
122
|
+
else:
|
|
123
|
+
if actual in (lower, upper):
|
|
124
|
+
raise MockgunError('relative datetime endpoint is not settled')
|
|
125
|
+
matches = lower < actual < upper
|
|
126
|
+
return not matches if negative else matches
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def validate_relative(
|
|
130
|
+
path: str,
|
|
131
|
+
field_type: str,
|
|
132
|
+
relation: str,
|
|
133
|
+
values: Sequence[object],
|
|
134
|
+
*,
|
|
135
|
+
context: RequestContext,
|
|
136
|
+
) -> None:
|
|
137
|
+
"""Validate relative operands and row-independent timezone and shift constraints.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
path: Root entity type and filter path, as `Entity.field`, for
|
|
141
|
+
qualified errors.
|
|
142
|
+
field_type: Either `date` or `date_time`.
|
|
143
|
+
relation: Supported relative relation.
|
|
144
|
+
values: Amount and unit, or calendar offset.
|
|
145
|
+
context: Request whose UTC time and session classification apply.
|
|
146
|
+
|
|
147
|
+
Raises:
|
|
148
|
+
ServerFaultError: A characterized invalid operand has fault code 103.
|
|
149
|
+
MockgunError: Operand, timezone, or calendar shift behavior is
|
|
150
|
+
unsettled.
|
|
151
|
+
"""
|
|
152
|
+
prefix = f'API read() {path} {relation}'
|
|
153
|
+
calendar = relation.startswith('in_calendar_')
|
|
154
|
+
valid = _valid_operands(field_type, relation, values, prefix)
|
|
155
|
+
prefix = f'mockgun cannot evaluate {path} {field_type} {relation}'
|
|
156
|
+
if not valid:
|
|
157
|
+
raise MockgunError(f'{prefix} operands are not settled: {values!r}')
|
|
158
|
+
unknown_zone = context.session_kind != 'script'
|
|
159
|
+
if unknown_zone and (calendar or field_type == 'date' or values[1] in {'MONTH', 'YEAR'}):
|
|
160
|
+
raise MockgunError(f'{prefix} requires a known session timezone')
|
|
161
|
+
try:
|
|
162
|
+
_window(field_type, relation, values, context.now)
|
|
163
|
+
except (ValueError, OverflowError) as exc:
|
|
164
|
+
raise MockgunError(f'{prefix} calendar shift is not settled: {exc}') from exc
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""Separate unsupported mockgun behavior from emulated server rejections."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class MockgunError(Exception):
|
|
5
|
+
"""Report mockgun setup failures and behavior the fake site cannot emulate."""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ServerFaultError(Exception):
|
|
9
|
+
"""Represent a server rejection encoded as a JSON-RPC fault.
|
|
10
|
+
|
|
11
|
+
Attributes:
|
|
12
|
+
error_code: Optional server error code accompanying the message.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
def __init__(self, message: str, error_code: int | None = None) -> None:
|
|
16
|
+
"""Initialize a rejection with its wire message and optional code."""
|
|
17
|
+
super().__init__(message)
|
|
18
|
+
self.error_code = error_code
|