sg-python-api 0.1.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.
@@ -0,0 +1,1205 @@
1
+ """Shared pure plans for synchronous and asynchronous client methods."""
2
+
3
+ import contextvars
4
+ import dataclasses
5
+ import datetime
6
+ import inspect
7
+ import warnings
8
+ from collections.abc import Callable, Generator
9
+ from contextlib import contextmanager
10
+ from typing import Any, NotRequired, TypedDict, cast
11
+
12
+ from . import _crud, _protocol
13
+ from ._config import CompatibilityWarningCode
14
+ from ._errors import ShotgunError
15
+ from ._signatures import bind_arguments, expose_signature
16
+
17
+ _ACTIVITY_STREAM_READ_DOCSTRING = """Read the activity stream for one entity.
18
+
19
+ Args:
20
+ entity_type: Entity type whose activity stream to read.
21
+ entity_id: Numeric identifier of the entity.
22
+ entity_fields: Optional fields to return for linked entity types.
23
+ min_id: Optional minimum activity identifier.
24
+ max_id: Optional maximum activity identifier.
25
+ limit: Optional maximum number of activity records.
26
+
27
+ Returns:
28
+ Activity stream data returned by the server.
29
+
30
+ Raises:
31
+ TypeError: If arguments do not match the documented signature.
32
+ ShotgunError: If an RPC fault or terminal transport failure
33
+ occurs, or if the server result has the wrong type.
34
+ ProtocolError: If a server request returns an HTTP error.
35
+ ValueError: If authenticated HTTP violates the configured origin policy.
36
+ """
37
+ _ACTIVITY_STREAM_READ_PARAMETERS = (
38
+ inspect.Parameter('entity_type', inspect.Parameter.POSITIONAL_OR_KEYWORD, annotation=str),
39
+ inspect.Parameter('entity_id', inspect.Parameter.POSITIONAL_OR_KEYWORD, annotation=int),
40
+ inspect.Parameter(
41
+ 'entity_fields',
42
+ inspect.Parameter.POSITIONAL_OR_KEYWORD,
43
+ default=None,
44
+ annotation=dict[str, list[str]] | None,
45
+ ),
46
+ inspect.Parameter(
47
+ 'min_id',
48
+ inspect.Parameter.POSITIONAL_OR_KEYWORD,
49
+ default=None,
50
+ annotation=int | None,
51
+ ),
52
+ inspect.Parameter(
53
+ 'max_id',
54
+ inspect.Parameter.POSITIONAL_OR_KEYWORD,
55
+ default=None,
56
+ annotation=int | None,
57
+ ),
58
+ inspect.Parameter(
59
+ 'limit',
60
+ inspect.Parameter.POSITIONAL_OR_KEYWORD,
61
+ default=None,
62
+ annotation=int | None,
63
+ ),
64
+ )
65
+ _ACTIVITY_STREAM_READ_SIGNATURE = inspect.Signature(
66
+ _ACTIVITY_STREAM_READ_PARAMETERS,
67
+ return_annotation=dict[str, object],
68
+ )
69
+ _EXPORT_PAGE_DOCSTRING = """Export a page using the documented arguments.
70
+
71
+ Args:
72
+ page_id: Numeric page identifier.
73
+ format: Required nonempty export format name.
74
+ layout_name: Optional layout name, including an empty name.
75
+
76
+ Returns:
77
+ Exported page text returned by the server.
78
+
79
+ Raises:
80
+ TypeError: If arguments do not match the documented signature.
81
+ ShotgunError: If an RPC fault or terminal transport failure
82
+ occurs, or if `page_id`, `format`, or the server result is invalid.
83
+ ProtocolError: If a server request returns an HTTP error.
84
+ ValueError: If authenticated HTTP violates the configured origin policy.
85
+ """
86
+ _EXPORT_PAGE_PARAMETERS = (
87
+ inspect.Parameter('page_id', inspect.Parameter.POSITIONAL_OR_KEYWORD, annotation=int),
88
+ inspect.Parameter('format', inspect.Parameter.POSITIONAL_OR_KEYWORD, annotation=str),
89
+ inspect.Parameter(
90
+ 'layout_name',
91
+ inspect.Parameter.POSITIONAL_OR_KEYWORD,
92
+ default=None,
93
+ annotation=str | None,
94
+ ),
95
+ )
96
+ _EXPORT_PAGE_SIGNATURE = inspect.Signature(
97
+ _EXPORT_PAGE_PARAMETERS,
98
+ return_annotation=str,
99
+ )
100
+ _METHOD_ACTIVITY_STREAM_READ_SIGNATURE = inspect.Signature(
101
+ (
102
+ inspect.Parameter('self', inspect.Parameter.POSITIONAL_OR_KEYWORD),
103
+ *_ACTIVITY_STREAM_READ_PARAMETERS,
104
+ ),
105
+ return_annotation=dict[str, object],
106
+ )
107
+ _METHOD_EXPORT_PAGE_SIGNATURE = inspect.Signature(
108
+ (
109
+ inspect.Parameter('self', inspect.Parameter.POSITIONAL_OR_KEYWORD),
110
+ *_EXPORT_PAGE_PARAMETERS,
111
+ ),
112
+ return_annotation=str,
113
+ )
114
+ _PENDING_COMPATIBILITY_WARNINGS = tuple[
115
+ object,
116
+ tuple[CompatibilityWarningCode, ...],
117
+ ]
118
+ _SCHEDULE_DATE_LENGTH = 10
119
+ _SUMMARIZE_DOCSTRING = """Summarize field data returned by a query.
120
+
121
+ Args:
122
+ entity_type: Entity type to summarize.
123
+ filters: Filters in list or dict syntax.
124
+ summary_fields: List of `{'field': ..., 'type': ...}` summaries.
125
+ filter_operator: `'all'` or `'any'` for list-syntax filters.
126
+ grouping: Optional grouping specs.
127
+ include_archived_projects: Include archived projects when `True`.
128
+
129
+ Returns:
130
+ Dict with `summaries` and (when grouped) `groups`.
131
+
132
+ Raises:
133
+ TypeError: If arguments do not match the documented signature.
134
+ ShotgunError: If an RPC fault or terminal transport failure
135
+ occurs, or if `filters`, `summary_fields`, or the server result has the
136
+ wrong type.
137
+ ProtocolError: If a server request returns an HTTP error.
138
+ ValueError: If authenticated HTTP violates the configured origin policy.
139
+ """
140
+ _SUMMARIZE_PARAMETERS = (
141
+ inspect.Parameter('entity_type', inspect.Parameter.POSITIONAL_OR_KEYWORD, annotation=str),
142
+ inspect.Parameter(
143
+ 'filters',
144
+ inspect.Parameter.POSITIONAL_OR_KEYWORD,
145
+ annotation=list[Any] | tuple[Any, ...] | dict[str, Any],
146
+ ),
147
+ inspect.Parameter(
148
+ 'summary_fields',
149
+ inspect.Parameter.POSITIONAL_OR_KEYWORD,
150
+ annotation=list[dict[str, Any]],
151
+ ),
152
+ inspect.Parameter(
153
+ 'filter_operator',
154
+ inspect.Parameter.POSITIONAL_OR_KEYWORD,
155
+ default=None,
156
+ annotation=str | None,
157
+ ),
158
+ inspect.Parameter(
159
+ 'grouping',
160
+ inspect.Parameter.POSITIONAL_OR_KEYWORD,
161
+ default=None,
162
+ annotation=list[dict[str, Any]] | None,
163
+ ),
164
+ inspect.Parameter(
165
+ 'include_archived_projects',
166
+ inspect.Parameter.POSITIONAL_OR_KEYWORD,
167
+ default=True,
168
+ annotation=bool,
169
+ ),
170
+ )
171
+ _METHOD_SUMMARIZE_SIGNATURE = inspect.Signature(
172
+ (
173
+ inspect.Parameter('self', inspect.Parameter.POSITIONAL_OR_KEYWORD),
174
+ *_SUMMARIZE_PARAMETERS,
175
+ ),
176
+ return_annotation=dict[str, object],
177
+ )
178
+ _SUMMARIZE_SIGNATURE = inspect.Signature(
179
+ _SUMMARIZE_PARAMETERS,
180
+ return_annotation=dict[str, object],
181
+ )
182
+ _WORK_SCHEDULE_UPDATE_DOCSTRING = """Update a work schedule.
183
+
184
+ Args:
185
+ date: Date or exact `YYYY-MM-DD` string to update.
186
+ working: Whether the date is a working day.
187
+ description: Optional schedule description.
188
+ project: Optional project entity restriction.
189
+ user: Optional user entity restriction.
190
+ recalculate_field: Optional field to recalculate.
191
+
192
+ Returns:
193
+ Updated work schedule data returned by the server.
194
+
195
+ Raises:
196
+ TypeError: If arguments do not match the documented signature.
197
+ ShotgunError: If an RPC fault or terminal transport failure
198
+ occurs, or if the date, project/user exclusivity, or server result is
199
+ invalid.
200
+ ProtocolError: If a server request returns an HTTP error.
201
+ ValueError: If authenticated HTTP violates the configured origin policy.
202
+ """
203
+ _WORK_SCHEDULE_UPDATE_PARAMETERS = (
204
+ inspect.Parameter(
205
+ 'date', inspect.Parameter.POSITIONAL_OR_KEYWORD, annotation=datetime.date | str
206
+ ),
207
+ inspect.Parameter('working', inspect.Parameter.POSITIONAL_OR_KEYWORD, annotation=bool),
208
+ inspect.Parameter(
209
+ 'description',
210
+ inspect.Parameter.POSITIONAL_OR_KEYWORD,
211
+ default=None,
212
+ annotation=str | None,
213
+ ),
214
+ inspect.Parameter(
215
+ 'project',
216
+ inspect.Parameter.POSITIONAL_OR_KEYWORD,
217
+ default=None,
218
+ annotation=dict[str, Any] | None,
219
+ ),
220
+ inspect.Parameter(
221
+ 'user',
222
+ inspect.Parameter.POSITIONAL_OR_KEYWORD,
223
+ default=None,
224
+ annotation=dict[str, Any] | None,
225
+ ),
226
+ inspect.Parameter(
227
+ 'recalculate_field',
228
+ inspect.Parameter.POSITIONAL_OR_KEYWORD,
229
+ default=None,
230
+ annotation=str | None,
231
+ ),
232
+ )
233
+ _METHOD_WORK_SCHEDULE_UPDATE_SIGNATURE = inspect.Signature(
234
+ (
235
+ inspect.Parameter('self', inspect.Parameter.POSITIONAL_OR_KEYWORD),
236
+ *_WORK_SCHEDULE_UPDATE_PARAMETERS,
237
+ ),
238
+ return_annotation=dict[str, object],
239
+ )
240
+ _WORK_SCHEDULE_UPDATE_SIGNATURE = inspect.Signature(
241
+ _WORK_SCHEDULE_UPDATE_PARAMETERS,
242
+ return_annotation=dict[str, object],
243
+ )
244
+ SCHEMA_ENTITY_READ_WARNINGS: contextvars.ContextVar[_PENDING_COMPATIBILITY_WARNINGS | None] = (
245
+ contextvars.ContextVar('SCHEMA_ENTITY_READ_WARNINGS', default=None)
246
+ )
247
+ SCHEMA_FIELD_READ_WARNINGS: contextvars.ContextVar[_PENDING_COMPATIBILITY_WARNINGS | None] = (
248
+ contextvars.ContextVar('SCHEMA_FIELD_READ_WARNINGS', default=None)
249
+ )
250
+
251
+
252
+ @dataclasses.dataclass(frozen=True)
253
+ class _ActivityStreamReadOptions:
254
+ """Collect exact public activity stream arguments for validation."""
255
+
256
+ entity_type: str
257
+ entity_id: int
258
+ entity_fields: dict[str, list[str]] | None = None
259
+ min_id: int | None = None
260
+ max_id: int | None = None
261
+ limit: int | None = None
262
+
263
+
264
+ @dataclasses.dataclass(frozen=True)
265
+ class _AuthenticationPlan:
266
+ """Carry one human-authentication request and its credentials."""
267
+
268
+ auth_spec: _protocol.AuthSpec
269
+ params: dict[str, object]
270
+
271
+
272
+ class ExportPageKeywordArguments(TypedDict):
273
+ """Describe supported keyword calls to `export_page`.
274
+
275
+ Attributes:
276
+ format: Required nonempty export format.
277
+ layout_name: Optional page layout name.
278
+ """
279
+
280
+ format: str
281
+ layout_name: NotRequired[str | None]
282
+
283
+
284
+ @dataclasses.dataclass(frozen=True)
285
+ class _ExportPageOptions:
286
+ """Collect exact public export-page arguments for validation."""
287
+
288
+ page_id: int
289
+ format: str
290
+ layout_name: str | None = None
291
+
292
+
293
+ @dataclasses.dataclass(frozen=True)
294
+ class _SummarizeOptions:
295
+ """Collect exact public `summarize` arguments for runtime validation."""
296
+
297
+ entity_type: str
298
+ filters: object
299
+ summary_fields: object
300
+ filter_operator: str | None = None
301
+ grouping: list[dict[str, object]] | None = None
302
+ include_archived_projects: bool = True
303
+
304
+
305
+ @dataclasses.dataclass(frozen=True)
306
+ class _WorkScheduleUpdateOptions:
307
+ """Collect exact public work-schedule update arguments for validation."""
308
+
309
+ date: object
310
+ working: bool
311
+ description: str | None = None
312
+ project: dict[str, object] | None = None
313
+ user: dict[str, object] | None = None
314
+ recalculate_field: str | None = None
315
+
316
+
317
+ def _date_str(value: object, argument: str) -> str:
318
+ """Return an exact schedule date string."""
319
+ if isinstance(value, datetime.datetime):
320
+ raise ShotgunError(f'{argument} must be a date, not a datetime')
321
+ if isinstance(value, datetime.date):
322
+ return f'{value.year:04d}-{value.month:02d}-{value.day:02d}'
323
+ if not isinstance(value, str):
324
+ raise ShotgunError(f'{argument} must be a datetime.date or YYYY-MM-DD string')
325
+ if (
326
+ len(value) != _SCHEDULE_DATE_LENGTH
327
+ or value[4] != '-'
328
+ or value[7] != '-'
329
+ or not value.isascii()
330
+ or not (value[:4] + value[5:7] + value[8:]).isdigit()
331
+ ):
332
+ raise ShotgunError(f'{argument} must be YYYY-MM-DD')
333
+ try:
334
+ datetime.date.fromisoformat(value)
335
+ except ValueError:
336
+ raise ShotgunError(f'{argument} must be YYYY-MM-DD') from None
337
+ return value
338
+
339
+
340
+ def _entity(value: object) -> object:
341
+ """Optimize an entity value for client request preparation."""
342
+ return _protocol.optimize_entity(value)
343
+
344
+
345
+ def _project_entity(value: object) -> object:
346
+ """Optimize an optional project entity."""
347
+ return None if value is None else _entity(value)
348
+
349
+
350
+ def _property_pairs(properties: dict[str, object]) -> list[dict[str, object]]:
351
+ """Build schema property entries in caller order."""
352
+ return [{'property_name': name, 'value': value} for name, value in properties.items()]
353
+
354
+
355
+ def _reject_project_and_user(
356
+ project: dict[str, object] | None,
357
+ user: dict[str, object] | None,
358
+ ) -> None:
359
+ """Reject simultaneous project and user restrictions."""
360
+ if project is not None and user is not None:
361
+ raise ShotgunError('project and user can only be used exclusively of each other')
362
+
363
+
364
+ def _require_nonempty_string(value: object, message: str) -> str:
365
+ """Validate a nonempty string."""
366
+ if not isinstance(value, str) or not value:
367
+ raise ShotgunError(message)
368
+ return value
369
+
370
+
371
+ def bind_activity_stream_read(
372
+ args: tuple[object, ...],
373
+ kwargs: dict[str, object],
374
+ owner_name: str,
375
+ ) -> _ActivityStreamReadOptions:
376
+ """Bind runtime activity-stream arguments.
377
+
378
+ Args:
379
+ args: Positional arguments supplied by the caller.
380
+ kwargs: Keyword arguments supplied by the caller.
381
+ owner_name: Public client class name used in errors.
382
+
383
+ Returns:
384
+ Bound activity-stream options.
385
+
386
+ Raises:
387
+ TypeError: If the arguments do not match the public signature.
388
+ """
389
+ bound = bind_arguments(
390
+ _ACTIVITY_STREAM_READ_SIGNATURE, args, kwargs, f'{owner_name}.activity_stream_read()'
391
+ )
392
+ options = cast('Callable[..., _ActivityStreamReadOptions]', _ActivityStreamReadOptions)
393
+ return options(**bound.arguments)
394
+
395
+
396
+ def bind_export_page(
397
+ args: tuple[object, ...],
398
+ kwargs: dict[str, object],
399
+ owner_name: str,
400
+ ) -> _ExportPageOptions:
401
+ """Bind runtime export-page arguments.
402
+
403
+ Args:
404
+ args: Positional arguments supplied by the caller.
405
+ kwargs: Keyword arguments supplied by the caller.
406
+ owner_name: Public client class name used in errors.
407
+
408
+ Returns:
409
+ Bound export-page options.
410
+
411
+ Raises:
412
+ TypeError: If the arguments do not match the public signature.
413
+ """
414
+ bound = bind_arguments(_EXPORT_PAGE_SIGNATURE, args, kwargs, f'{owner_name}.export_page()')
415
+ options = cast('Callable[..., _ExportPageOptions]', _ExportPageOptions)
416
+ return options(**bound.arguments)
417
+
418
+
419
+ def bind_summarize(
420
+ args: tuple[object, ...],
421
+ kwargs: dict[str, object],
422
+ owner_name: str,
423
+ ) -> _SummarizeOptions:
424
+ """Bind runtime summarize arguments.
425
+
426
+ Args:
427
+ args: Positional arguments supplied by the caller.
428
+ kwargs: Keyword arguments supplied by the caller.
429
+ owner_name: Public client class name used in errors.
430
+
431
+ Returns:
432
+ Bound summarize options.
433
+
434
+ Raises:
435
+ TypeError: If the arguments do not match the public signature.
436
+ """
437
+ bound = bind_arguments(_SUMMARIZE_SIGNATURE, args, kwargs, f'{owner_name}.summarize()')
438
+ options = cast('Callable[..., _SummarizeOptions]', _SummarizeOptions)
439
+ return options(**bound.arguments)
440
+
441
+
442
+ def bind_work_schedule_update(
443
+ args: tuple[object, ...],
444
+ kwargs: dict[str, object],
445
+ owner_name: str,
446
+ ) -> _WorkScheduleUpdateOptions:
447
+ """Bind runtime work-schedule update arguments.
448
+
449
+ Args:
450
+ args: Positional arguments supplied by the caller.
451
+ kwargs: Keyword arguments supplied by the caller.
452
+ owner_name: Public client class name used in errors.
453
+
454
+ Returns:
455
+ Bound schedule-update options.
456
+
457
+ Raises:
458
+ TypeError: If the arguments do not match the public signature.
459
+ """
460
+ bound = bind_arguments(
461
+ _WORK_SCHEDULE_UPDATE_SIGNATURE, args, kwargs, f'{owner_name}.work_schedule_update()'
462
+ )
463
+ options = cast('Callable[..., _WorkScheduleUpdateOptions]', _WorkScheduleUpdateOptions)
464
+ return options(**bound.arguments)
465
+
466
+
467
+ def build_activity_stream_read_params(options: _ActivityStreamReadOptions) -> dict[str, object]:
468
+ """Build activity-stream parameters.
469
+
470
+ Args:
471
+ options: Bound activity-stream options.
472
+
473
+ Returns:
474
+ RPC parameters copied by the client before dispatch.
475
+ """
476
+ fields = options.entity_fields
477
+ return {
478
+ 'type': options.entity_type,
479
+ 'id': options.entity_id,
480
+ 'max_id': options.max_id,
481
+ 'min_id': options.min_id,
482
+ 'limit': options.limit,
483
+ 'entity_fields': {} if fields is None else fields,
484
+ }
485
+
486
+
487
+ def build_authentication_plan(
488
+ user_login: str,
489
+ user_password: str,
490
+ auth_token: str | None,
491
+ ) -> _AuthenticationPlan:
492
+ """Build one human-authentication operation.
493
+
494
+ Args:
495
+ user_login: Login to authenticate.
496
+ user_password: Password to authenticate.
497
+ auth_token: Optional two-factor token.
498
+
499
+ Returns:
500
+ Request parameters and per-request authentication specification.
501
+ """
502
+ params = _crud.build_find_plan(
503
+ _crud.prepare_find(
504
+ _crud.FindOptions(
505
+ entity_type='HumanUser',
506
+ filters=[
507
+ ['sg_status_list', 'is', 'act'],
508
+ ['login', 'is', user_login],
509
+ ],
510
+ fields=['id', 'login'],
511
+ order=None,
512
+ filter_operator=None,
513
+ limit=1,
514
+ retired_only=False,
515
+ page=1,
516
+ include_archived_projects=True,
517
+ additional_filter_presets=None,
518
+ )
519
+ ),
520
+ 1,
521
+ ).params
522
+ return _AuthenticationPlan(
523
+ auth_spec=_protocol.AuthSpec(
524
+ script_name=None,
525
+ api_key=None,
526
+ user_login=user_login,
527
+ user_password=user_password,
528
+ session_token=None,
529
+ auth_token=auth_token,
530
+ sudo_as_login=None,
531
+ session_uuid=None,
532
+ extra_auth_params=None,
533
+ ),
534
+ params=params,
535
+ )
536
+
537
+
538
+ def build_export_page_params(options: _ExportPageOptions) -> dict[str, object]:
539
+ """Build validated page-export parameters.
540
+
541
+ Args:
542
+ options: Bound export-page options.
543
+
544
+ Returns:
545
+ RPC parameters copied by the client before dispatch.
546
+
547
+ Raises:
548
+ ShotgunError: If the page identifier or format is invalid.
549
+ """
550
+ page_id = _crud.require_entity_id(
551
+ options.page_id,
552
+ 'export_page page_id must be an integer',
553
+ )
554
+ page_format = _require_nonempty_string(
555
+ options.format,
556
+ 'export_page format must be a nonempty string',
557
+ )
558
+ params: dict[str, object] = {'page_id': page_id, 'format': page_format}
559
+ if options.layout_name is not None:
560
+ params['layout_name'] = options.layout_name
561
+ return params
562
+
563
+
564
+ def build_follow_params(user: object, target: object) -> dict[str, object]:
565
+ """Build follow or unfollow parameters.
566
+
567
+ Args:
568
+ user: User entity.
569
+ target: Target entity.
570
+
571
+ Returns:
572
+ RPC parameters copied by the client before dispatch.
573
+ """
574
+ return {'user': _entity(user), 'entity': _entity(target)}
575
+
576
+
577
+ def build_followers_params(target: object) -> dict[str, object]:
578
+ """Build follower-query parameters.
579
+
580
+ Args:
581
+ target: Target entity.
582
+
583
+ Returns:
584
+ RPC parameters copied by the client before dispatch.
585
+ """
586
+ return {'entity': _entity(target)}
587
+
588
+
589
+ def build_following_params(
590
+ user: object,
591
+ project: object,
592
+ entity_type: object,
593
+ ) -> dict[str, object]:
594
+ """Build following-query parameters.
595
+
596
+ Args:
597
+ user: User entity.
598
+ project: Optional project entity.
599
+ entity_type: Optional entity type restriction.
600
+
601
+ Returns:
602
+ RPC parameters copied by the client before dispatch.
603
+ """
604
+ params: dict[str, object] = {'user': _entity(user)}
605
+ if project is not None:
606
+ params['project'] = _entity(project)
607
+ if entity_type is not None:
608
+ params['entity_type'] = entity_type
609
+ return params
610
+
611
+
612
+ def build_nav_expand_params(
613
+ path: object,
614
+ seed_entity_field: object,
615
+ requested_entity_fields: object,
616
+ ) -> dict[str, object]:
617
+ """Build navigation expansion parameters.
618
+
619
+ Args:
620
+ path: Navigation path.
621
+ seed_entity_field: Optional seed field.
622
+ requested_entity_fields: Optional linked-entity fields.
623
+
624
+ Returns:
625
+ RPC parameters copied by the client before dispatch.
626
+ """
627
+ fields = requested_entity_fields
628
+ params: dict[str, object] = {'path': path}
629
+ if seed_entity_field is not None:
630
+ params['seed_entity_field'] = seed_entity_field
631
+ if fields is not None:
632
+ params['entity_fields'] = fields
633
+ return params
634
+
635
+
636
+ def build_nav_search_entity_params(
637
+ root_path: object,
638
+ searched_entity: object,
639
+ seed_entity_field: object,
640
+ ) -> dict[str, object]:
641
+ """Build entity navigation-search parameters.
642
+
643
+ Args:
644
+ root_path: Navigation root path.
645
+ searched_entity: Entity to find.
646
+ seed_entity_field: Optional seed field.
647
+
648
+ Returns:
649
+ RPC parameters copied by the client before dispatch.
650
+ """
651
+ params: dict[str, object] = {
652
+ 'root_path': root_path,
653
+ 'search_criteria': {'entity': _entity(searched_entity)},
654
+ }
655
+ if seed_entity_field is not None:
656
+ params['seed_entity_field'] = seed_entity_field
657
+ return params
658
+
659
+
660
+ def build_nav_search_string_params(
661
+ root_path: object,
662
+ search_string: object,
663
+ seed_entity_field: object,
664
+ ) -> dict[str, object]:
665
+ """Build string navigation-search parameters.
666
+
667
+ Args:
668
+ root_path: Navigation root path.
669
+ search_string: Search text.
670
+ seed_entity_field: Optional seed field.
671
+
672
+ Returns:
673
+ RPC parameters copied by the client before dispatch.
674
+ """
675
+ params: dict[str, object] = {
676
+ 'root_path': root_path,
677
+ 'search_criteria': {'search_string': search_string},
678
+ }
679
+ if seed_entity_field is not None:
680
+ params['seed_entity_field'] = seed_entity_field
681
+ return params
682
+
683
+
684
+ def build_note_thread_read_params(
685
+ note_id: object,
686
+ requested_entity_fields: object,
687
+ ) -> dict[str, object]:
688
+ """Build note-thread parameters.
689
+
690
+ Args:
691
+ note_id: Note identifier.
692
+ requested_entity_fields: Optional linked-entity fields.
693
+
694
+ Returns:
695
+ RPC parameters copied by the client before dispatch.
696
+ """
697
+ fields = requested_entity_fields
698
+ params: dict[str, object] = {'note_id': note_id}
699
+ if fields is not None:
700
+ params['entity_fields'] = fields
701
+ return params
702
+
703
+
704
+ def build_preferences_read_params(prefs: object) -> dict[str, object]:
705
+ """Build preference-read parameters.
706
+
707
+ Args:
708
+ prefs: Optional preference names.
709
+
710
+ Returns:
711
+ RPC parameters copied by the client before dispatch.
712
+ """
713
+ return {'prefs': prefs or []}
714
+
715
+
716
+ def build_schema_entity_read_params(project: object) -> dict[str, object] | None:
717
+ """Build entity-schema parameters.
718
+
719
+ Args:
720
+ project: Optional project entity.
721
+
722
+ Returns:
723
+ RPC parameters or `None`, copied by the client before dispatch.
724
+ """
725
+ project_entity = _project_entity(project)
726
+ return {'project': project_entity} if project_entity is not None else None
727
+
728
+
729
+ def build_schema_field_create_params(
730
+ entity_type: object,
731
+ data_type: object,
732
+ display_name: object,
733
+ properties: dict[str, object] | None,
734
+ ) -> dict[str, object]:
735
+ """Build field-creation parameters.
736
+
737
+ Args:
738
+ entity_type: Owning entity type.
739
+ data_type: New field data type.
740
+ display_name: New field display name.
741
+ properties: Optional field properties.
742
+
743
+ Returns:
744
+ RPC parameters copied by the client before dispatch.
745
+ """
746
+ field_properties = {} if properties is None else properties
747
+ return {
748
+ 'type': entity_type,
749
+ 'data_type': data_type,
750
+ 'properties': [
751
+ {'property_name': 'name', 'value': display_name},
752
+ *_property_pairs(field_properties),
753
+ ],
754
+ }
755
+
756
+
757
+ def build_schema_field_delete_params(
758
+ entity_type: object,
759
+ field_name: object,
760
+ ) -> dict[str, object]:
761
+ """Build field-deletion parameters.
762
+
763
+ Args:
764
+ entity_type: Owning entity type.
765
+ field_name: Internal field name.
766
+
767
+ Returns:
768
+ RPC parameters copied by the client before dispatch.
769
+ """
770
+ return {
771
+ 'type': entity_type,
772
+ 'field_name': field_name,
773
+ }
774
+
775
+
776
+ def build_schema_field_read_params(
777
+ entity_type: object,
778
+ field_name: object,
779
+ project: object,
780
+ ) -> dict[str, object]:
781
+ """Build field-schema parameters.
782
+
783
+ Args:
784
+ entity_type: Owning entity type.
785
+ field_name: Optional internal field name.
786
+ project: Optional project entity.
787
+
788
+ Returns:
789
+ RPC parameters copied by the client before dispatch.
790
+ """
791
+ project_entity = _project_entity(project)
792
+ params: dict[str, object] = {'type': entity_type}
793
+ if field_name is not None:
794
+ params['field_name'] = field_name
795
+ if project_entity is not None:
796
+ params['project'] = project_entity
797
+ return params
798
+
799
+
800
+ def build_schema_field_update_params(
801
+ entity_type: object,
802
+ field_name: object,
803
+ properties: dict[str, object],
804
+ project: object,
805
+ ) -> dict[str, object]:
806
+ """Build field-update parameters.
807
+
808
+ Args:
809
+ entity_type: Owning entity type.
810
+ field_name: Internal field name.
811
+ properties: Field properties to update.
812
+ project: Optional project entity.
813
+
814
+ Returns:
815
+ RPC parameters copied by the client before dispatch.
816
+ """
817
+ project_entity = _project_entity(project)
818
+ params: dict[str, object] = {
819
+ 'type': entity_type,
820
+ 'field_name': field_name,
821
+ 'properties': _property_pairs(properties),
822
+ }
823
+ if project_entity is not None:
824
+ params['project'] = project_entity
825
+ return params
826
+
827
+
828
+ def build_summarize_params(options: _SummarizeOptions) -> dict[str, object]:
829
+ """Build validated summarize parameters.
830
+
831
+ Args:
832
+ options: Bound summarize options.
833
+
834
+ Returns:
835
+ RPC parameters copied by the client before dispatch.
836
+
837
+ Raises:
838
+ ShotgunError: If filters or summary fields are invalid.
839
+ """
840
+ raw_filters: object = options.filters
841
+ if not isinstance(raw_filters, (list, tuple, dict)):
842
+ raise ShotgunError('summarize() requires list or dict filters')
843
+ filters = cast(
844
+ 'list[object] | tuple[object, ...] | dict[str, object]',
845
+ raw_filters,
846
+ )
847
+ summary_fields = cast(
848
+ 'list[dict[str, object]]',
849
+ _crud.require_list(
850
+ options.summary_fields,
851
+ 'summarize() requires a list of summary fields',
852
+ ),
853
+ )
854
+ params: dict[str, object] = {
855
+ 'type': options.entity_type,
856
+ 'filters': _protocol.translate_filters(filters, options.filter_operator),
857
+ 'summaries': summary_fields,
858
+ }
859
+ if options.grouping is not None:
860
+ params['grouping'] = options.grouping
861
+ if not options.include_archived_projects:
862
+ params['include_archived_projects'] = False
863
+ return params
864
+
865
+
866
+ def build_text_search_params(
867
+ text: str,
868
+ entity_types: object,
869
+ project_ids: object,
870
+ limit: object,
871
+ ) -> dict[str, object]:
872
+ """Build validated text-search parameters.
873
+
874
+ Args:
875
+ text: Search text.
876
+ entity_types: Entity-type filter mapping.
877
+ project_ids: Optional project identifiers.
878
+ limit: Optional maximum result count.
879
+
880
+ Returns:
881
+ RPC parameters copied by the client before dispatch.
882
+
883
+ Raises:
884
+ ShotgunError: If `entity_types` is not a dictionary.
885
+ """
886
+ validated_entity_types = _crud.require_dictionary(
887
+ entity_types,
888
+ 'text_search() entity_types must be a dict',
889
+ )
890
+ translated = {
891
+ entity_type: _protocol.translate_filters(_protocol.require_filter_container(filters))
892
+ for entity_type, filters in validated_entity_types.items()
893
+ }
894
+ return {
895
+ 'text': text,
896
+ 'entity_types': translated,
897
+ 'project_ids': [] if project_ids is None else project_ids,
898
+ 'max_results': limit,
899
+ }
900
+
901
+
902
+ def build_update_project_last_accessed_params(
903
+ project: dict[str, object],
904
+ user: dict[str, object] | None,
905
+ ) -> dict[str, object]:
906
+ """Build last-accessed parameters.
907
+
908
+ Args:
909
+ project: Accessed project entity.
910
+ user: Optional attributed user entity.
911
+
912
+ Returns:
913
+ RPC parameters copied by the client before dispatch.
914
+ """
915
+ params: dict[str, object] = {'project_id': project['id']}
916
+ if user is not None:
917
+ params['user_id'] = user['id']
918
+ return params
919
+
920
+
921
+ def build_user_subscriptions_create_params(users: object) -> dict[str, object]:
922
+ """Build validated subscription parameters.
923
+
924
+ Args:
925
+ users: User subscription entries.
926
+
927
+ Returns:
928
+ RPC parameters copied by the client before dispatch.
929
+
930
+ Raises:
931
+ ShotgunError: If the entries are invalid.
932
+ """
933
+ validated = _crud.require_list(
934
+ users,
935
+ 'user_subscriptions_create users must be a list',
936
+ )
937
+ message = 'user_subscriptions_create users must contain dictionaries with a user_id key'
938
+ for member in validated:
939
+ if not isinstance(member, dict) or 'user_id' not in member:
940
+ raise ShotgunError(message)
941
+ return {'users': cast('list[dict[str, object]]', validated)}
942
+
943
+
944
+ def build_work_schedule_read_params(
945
+ start_date: object,
946
+ end_date: object,
947
+ project: dict[str, object] | None,
948
+ user: dict[str, object] | None,
949
+ ) -> dict[str, object]:
950
+ """Build validated schedule-read parameters.
951
+
952
+ Args:
953
+ start_date: Inclusive `datetime.date` or exact `YYYY-MM-DD` string;
954
+ `datetime.datetime` is not accepted.
955
+ end_date: Inclusive `datetime.date` or exact `YYYY-MM-DD` string;
956
+ `datetime.datetime` is not accepted.
957
+ project: Optional project restriction.
958
+ user: Optional user restriction.
959
+
960
+ Returns:
961
+ RPC parameters copied by the client before dispatch.
962
+
963
+ Raises:
964
+ ShotgunError: If dates or restrictions are invalid.
965
+ """
966
+ params: dict[str, object] = {
967
+ 'start_date': _date_str(start_date, 'start_date'),
968
+ 'end_date': _date_str(end_date, 'end_date'),
969
+ }
970
+ if project is not None:
971
+ params['project'] = _entity(project)
972
+ if user is not None:
973
+ params['user'] = _entity(user)
974
+ return params
975
+
976
+
977
+ def build_work_schedule_update_params(
978
+ options: _WorkScheduleUpdateOptions,
979
+ ) -> dict[str, object]:
980
+ """Build validated schedule-update parameters.
981
+
982
+ Args:
983
+ options: Bound schedule-update options. The date must be a
984
+ `datetime.date` or exact `YYYY-MM-DD` string, not a
985
+ `datetime.datetime`. Project and user restrictions are mutually
986
+ exclusive.
987
+
988
+ Returns:
989
+ RPC parameters copied by the client before dispatch.
990
+
991
+ Raises:
992
+ ShotgunError: If the date is invalid or both restrictions are supplied.
993
+ """
994
+ _reject_project_and_user(options.project, options.user)
995
+ params: dict[str, object] = {
996
+ 'date': _date_str(options.date, 'date'),
997
+ 'working': options.working,
998
+ }
999
+ if options.description is not None:
1000
+ params['description'] = options.description
1001
+ if options.project is not None:
1002
+ params['project'] = _entity(options.project)
1003
+ if options.user is not None:
1004
+ params['user'] = _entity(options.user)
1005
+ if options.recalculate_field is not None:
1006
+ params['recalculate_field'] = options.recalculate_field
1007
+ return params
1008
+
1009
+
1010
+ def consume_compatibility_warnings(
1011
+ warning_context: contextvars.ContextVar[_PENDING_COMPATIBILITY_WARNINGS | None],
1012
+ owner: object,
1013
+ ) -> tuple[CompatibilityWarningCode, ...]:
1014
+ """Consume operation-local compatibility warnings exactly once.
1015
+
1016
+ Args:
1017
+ warning_context: Context-local warning state.
1018
+ owner: Client instance expected to own the warning.
1019
+
1020
+ Returns:
1021
+ Compatibility warning codes for the target call.
1022
+ """
1023
+ pending = warning_context.get()
1024
+ if pending is None or pending[0] is not owner:
1025
+ return ()
1026
+ warning_context.set(None)
1027
+ return pending[1]
1028
+
1029
+
1030
+ @contextmanager
1031
+ def entity_types_alias(owner: object) -> Generator[None, None, None]:
1032
+ """Scope the deprecated `entity_types` compatibility notice.
1033
+
1034
+ Args:
1035
+ owner: Client instance invoking the alias.
1036
+
1037
+ Yields:
1038
+ Control while the public target method consumes the warning.
1039
+ """
1040
+ token = SCHEMA_ENTITY_READ_WARNINGS.set(
1041
+ (owner, ('deprecated-entity-types-alias',)),
1042
+ )
1043
+ try:
1044
+ yield
1045
+ finally:
1046
+ SCHEMA_ENTITY_READ_WARNINGS.reset(token)
1047
+
1048
+
1049
+ def expose_activity_stream_read_signature(
1050
+ function: Callable[..., object],
1051
+ ) -> Callable[..., object]:
1052
+ """Expose the documented activity-stream signature.
1053
+
1054
+ Args:
1055
+ function: Variadic implementation.
1056
+
1057
+ Returns:
1058
+ The implementation with public reflection metadata.
1059
+ """
1060
+ return expose_signature(
1061
+ function, _METHOD_ACTIVITY_STREAM_READ_SIGNATURE, _ACTIVITY_STREAM_READ_DOCSTRING
1062
+ )
1063
+
1064
+
1065
+ def expose_export_page_signature(
1066
+ function: Callable[..., object],
1067
+ ) -> Callable[..., object]:
1068
+ """Expose the documented page-export signature.
1069
+
1070
+ Args:
1071
+ function: Variadic implementation.
1072
+
1073
+ Returns:
1074
+ The implementation with public reflection metadata.
1075
+ """
1076
+ return expose_signature(function, _METHOD_EXPORT_PAGE_SIGNATURE, _EXPORT_PAGE_DOCSTRING)
1077
+
1078
+
1079
+ def expose_summarize_signature(
1080
+ function: Callable[..., object],
1081
+ ) -> Callable[..., object]:
1082
+ """Expose the documented summarize signature.
1083
+
1084
+ Args:
1085
+ function: Variadic implementation.
1086
+
1087
+ Returns:
1088
+ The implementation with public reflection metadata.
1089
+ """
1090
+ return expose_signature(function, _METHOD_SUMMARIZE_SIGNATURE, _SUMMARIZE_DOCSTRING)
1091
+
1092
+
1093
+ def expose_work_schedule_update_signature(
1094
+ function: Callable[..., object],
1095
+ ) -> Callable[..., object]:
1096
+ """Expose the documented schedule-update signature.
1097
+
1098
+ Args:
1099
+ function: Variadic implementation.
1100
+
1101
+ Returns:
1102
+ The implementation with public reflection metadata.
1103
+ """
1104
+ return expose_signature(
1105
+ function, _METHOD_WORK_SCHEDULE_UPDATE_SIGNATURE, _WORK_SCHEDULE_UPDATE_DOCSTRING
1106
+ )
1107
+
1108
+
1109
+ def require_dictionary_list_result(
1110
+ value: object,
1111
+ method_name: str,
1112
+ ) -> list[dict[str, object]]:
1113
+ """Validate a list of dictionary RPC results.
1114
+
1115
+ Args:
1116
+ value: Raw RPC result.
1117
+ method_name: Public method name used in errors.
1118
+
1119
+ Returns:
1120
+ Validated dictionary list.
1121
+
1122
+ Raises:
1123
+ ShotgunError: If the result is invalid.
1124
+ """
1125
+ result = _crud.require_list_result(value, method_name)
1126
+ if any(not isinstance(member, dict) for member in result):
1127
+ raise ShotgunError(f'{method_name} returned a non-dictionary member')
1128
+ return cast('list[dict[str, object]]', result)
1129
+
1130
+
1131
+ def require_human_authentication_result(value: object) -> dict[str, object] | None:
1132
+ """Validate and select the first human-authentication result.
1133
+
1134
+ Args:
1135
+ value: Raw read result.
1136
+
1137
+ Returns:
1138
+ First matching user or `None`.
1139
+
1140
+ Raises:
1141
+ ShotgunError: If the result is invalid.
1142
+ """
1143
+ result = _crud.require_dictionary_result(value, 'authenticate_human_user')
1144
+ entities = _crud.require_list(
1145
+ result.get('entities', []),
1146
+ 'authenticate_human_user did not return an entities list',
1147
+ )
1148
+ if any(not isinstance(member, dict) for member in entities):
1149
+ raise ShotgunError('authenticate_human_user returned a non-dictionary entity')
1150
+ return cast('dict[str, object]', entities[0]) if entities else None
1151
+
1152
+
1153
+ def require_string_result(value: object, method_name: str) -> str:
1154
+ """Validate a string RPC result.
1155
+
1156
+ Args:
1157
+ value: Raw RPC result.
1158
+ method_name: Public method name used in errors.
1159
+
1160
+ Returns:
1161
+ Validated string.
1162
+
1163
+ Raises:
1164
+ ShotgunError: If the result is not a string.
1165
+ """
1166
+ if not isinstance(value, str):
1167
+ raise ShotgunError(f'{method_name} did not return a string')
1168
+ return value
1169
+
1170
+
1171
+ @contextmanager
1172
+ def schema_alias(owner: object) -> Generator[None, None, None]:
1173
+ """Scope the deprecated `schema` compatibility notice.
1174
+
1175
+ Args:
1176
+ owner: Client instance invoking the alias.
1177
+
1178
+ Yields:
1179
+ Control while the public target method consumes the warning.
1180
+ """
1181
+ token = SCHEMA_FIELD_READ_WARNINGS.set(
1182
+ (owner, ('deprecated-schema-alias',)),
1183
+ )
1184
+ try:
1185
+ yield
1186
+ finally:
1187
+ SCHEMA_FIELD_READ_WARNINGS.reset(token)
1188
+
1189
+
1190
+ def warn_entity_types_alias() -> None:
1191
+ """Warn at the caller of the deprecated `entity_types` method."""
1192
+ warnings.warn(
1193
+ 'entity_types() is deprecated; use schema_entity_read()',
1194
+ DeprecationWarning,
1195
+ stacklevel=3,
1196
+ )
1197
+
1198
+
1199
+ def warn_schema_alias() -> None:
1200
+ """Warn at the caller of the deprecated `schema` method."""
1201
+ warnings.warn(
1202
+ 'schema() is deprecated; use schema_field_read()',
1203
+ DeprecationWarning,
1204
+ stacklevel=3,
1205
+ )