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.
Files changed (56) hide show
  1. {sg_python_api-0.1.2.dist-info → sg_python_api-0.2.0.dist-info}/METADATA +21 -24
  2. sg_python_api-0.2.0.dist-info/RECORD +58 -0
  3. shotgun_api3/__init__.py +4 -0
  4. shotgun_api3/_async_client.py +347 -281
  5. shotgun_api3/_async_driver.py +25 -55
  6. shotgun_api3/_async_httpx2.py +92 -59
  7. shotgun_api3/_async_transport.py +38 -3
  8. shotgun_api3/_capabilities.py +4 -3
  9. shotgun_api3/_client.py +296 -422
  10. shotgun_api3/_config.py +245 -120
  11. shotgun_api3/_connections.py +144 -0
  12. shotgun_api3/_crud.py +25 -250
  13. shotgun_api3/_driver.py +29 -54
  14. shotgun_api3/_execution.py +1 -3
  15. shotgun_api3/_files.py +193 -21
  16. shotgun_api3/_methods.py +59 -411
  17. shotgun_api3/_protocol.py +27 -53
  18. shotgun_api3/_transport.py +82 -407
  19. shotgun_api3/_version.py +7 -2
  20. shotgun_api3/lib/mockgun/__init__.py +20 -7
  21. shotgun_api3/lib/mockgun/_activity.py +510 -0
  22. shotgun_api3/lib/mockgun/_async_client.py +196 -0
  23. shotgun_api3/lib/mockgun/_dates.py +164 -0
  24. shotgun_api3/lib/mockgun/_errors.py +18 -0
  25. shotgun_api3/lib/mockgun/_events.py +1031 -0
  26. shotgun_api3/lib/mockgun/_follows.py +217 -0
  27. shotgun_api3/lib/mockgun/_handlers.py +558 -0
  28. shotgun_api3/lib/mockgun/_links.py +440 -0
  29. shotgun_api3/lib/mockgun/_new_client.py +92 -0
  30. shotgun_api3/lib/mockgun/_presets.py +492 -0
  31. shotgun_api3/lib/mockgun/_query.py +2163 -0
  32. shotgun_api3/lib/mockgun/_retirement.py +368 -0
  33. shotgun_api3/lib/mockgun/_schema.py +172 -0
  34. shotgun_api3/lib/mockgun/_schema_writes.py +425 -0
  35. shotgun_api3/lib/mockgun/_search.py +378 -0
  36. shotgun_api3/lib/mockgun/_server.py +169 -0
  37. shotgun_api3/lib/mockgun/_sessions.py +206 -0
  38. shotgun_api3/lib/mockgun/_site.py +150 -0
  39. shotgun_api3/lib/mockgun/_site_data.py +584 -0
  40. shotgun_api3/lib/mockgun/_storage.py +208 -0
  41. shotgun_api3/lib/mockgun/_store.py +1446 -0
  42. shotgun_api3/lib/mockgun/_summaries.py +964 -0
  43. shotgun_api3/lib/mockgun/_sync_client.py +190 -0
  44. shotgun_api3/lib/mockgun/_threads.py +226 -0
  45. shotgun_api3/lib/mockgun/_thumbnails.py +146 -0
  46. shotgun_api3/lib/mockgun/_transport.py +145 -0
  47. shotgun_api3/lib/mockgun/_uploads.py +417 -0
  48. shotgun_api3/lib/mockgun/_wire.py +120 -0
  49. shotgun_api3/lib/mockgun/mockgun.py +14 -0
  50. shotgun_api3/lib/sgtimezone.py +24 -1
  51. sg_python_api-0.1.2.dist-info/RECORD +0 -31
  52. shotgun_api3/_signatures.py +0 -53
  53. shotgun_api3/lib/mockgun/_async.py +0 -315
  54. shotgun_api3/lib/mockgun/_mockgun.py +0 -1666
  55. {sg_python_api-0.1.2.dist-info → sg_python_api-0.2.0.dist-info}/WHEEL +0 -0
  56. {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