docket-client 0.3.2__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.
- docket/__init__.py +8 -0
- docket/api/__init__.py +1 -0
- docket/api/access/__init__.py +1 -0
- docket/api/access/add_principal.py +193 -0
- docket/api/access/archive_role_assignment.py +182 -0
- docket/api/access/create_api_token.py +198 -0
- docket/api/access/create_application_principal.py +188 -0
- docket/api/access/create_role_assignment.py +194 -0
- docket/api/access/list_api_tokens.py +184 -0
- docket/api/access/list_principals.py +184 -0
- docket/api/access/list_role_assignments.py +200 -0
- docket/api/access/revoke_api_token.py +173 -0
- docket/api/identity/__init__.py +1 -0
- docket/api/identity/create_my_api_token.py +206 -0
- docket/api/identity/get_me.py +127 -0
- docket/api/identity/list_my_api_tokens.py +151 -0
- docket/api/identity/revoke_my_api_token.py +178 -0
- docket/api/recurring_acquisitions/__init__.py +1 -0
- docket/api/recurring_acquisitions/archive_recurring_acquisition.py +207 -0
- docket/api/recurring_acquisitions/create_recurring_acquisition.py +218 -0
- docket/api/recurring_acquisitions/get_recurring_acquisition.py +163 -0
- docket/api/recurring_acquisitions/list_recurring_acquisitions.py +248 -0
- docket/api/recurring_acquisitions/preview_recurring_acquisition.py +187 -0
- docket/api/recurring_acquisitions/preview_split_recurring_acquisition.py +232 -0
- docket/api/recurring_acquisitions/split_recurring_acquisition.py +258 -0
- docket/api/runs/__init__.py +1 -0
- docket/api/runs/get_run.py +160 -0
- docket/api/runs/list_runs.py +180 -0
- docket/api/runs/report_run_outcome.py +206 -0
- docket/api/scheduled_acquisitions/__init__.py +1 -0
- docket/api/scheduled_acquisitions/archive_scheduled_acquisition.py +203 -0
- docket/api/scheduled_acquisitions/create_scheduled_acquisition.py +185 -0
- docket/api/scheduled_acquisitions/get_resolved_settings.py +169 -0
- docket/api/scheduled_acquisitions/get_scheduled_acquisition.py +159 -0
- docket/api/scheduled_acquisitions/list_active_acquisitions.py +297 -0
- docket/api/scheduled_acquisitions/list_runs_for_scheduled_acquisition.py +191 -0
- docket/api/scheduled_acquisitions/list_scheduled_acquisitions.py +332 -0
- docket/api/scheduled_acquisitions/preview_scheduled_acquisition_edit.py +224 -0
- docket/api/scheduled_acquisitions/set_planning_status.py +199 -0
- docket/api/scheduled_acquisitions/start_run.py +221 -0
- docket/api/scheduled_acquisitions/update_scheduled_acquisition.py +226 -0
- docket/api/settings/__init__.py +1 -0
- docket/api/settings/list_behavior_curriculum_discriminator_values.py +188 -0
- docket/api/settings/list_behavior_curriculum_settings.py +199 -0
- docket/api/settings/list_fip_discriminator_values.py +184 -0
- docket/api/settings/list_fip_settings.py +195 -0
- docket/api/settings/list_live_behavior_curriculum_settings.py +233 -0
- docket/api/settings/list_live_fip_settings.py +225 -0
- docket/api/settings/preview_behavior_curriculum_settings.py +191 -0
- docket/api/settings/preview_fip_settings.py +177 -0
- docket/api/settings/push_behavior_curriculum_settings.py +222 -0
- docket/api/settings/push_fip_settings.py +206 -0
- docket/api/templates/__init__.py +1 -0
- docket/api/templates/archive_template.py +203 -0
- docket/api/templates/create_template.py +206 -0
- docket/api/templates/get_template.py +159 -0
- docket/api/templates/list_templates.py +174 -0
- docket/api/templates/update_template.py +234 -0
- docket/api/vocabulary/__init__.py +1 -0
- docket/api/vocabulary/list_instruments.py +165 -0
- docket/api/vocabulary/list_persons.py +132 -0
- docket/api/vocabulary/list_projects.py +132 -0
- docket/api/vocabulary/list_subjects.py +132 -0
- docket/api/vocabulary/lookup_person.py +166 -0
- docket/api/vocabulary/lookup_subject.py +166 -0
- docket/api/vocabulary/sync_vocabulary.py +148 -0
- docket/client.py +272 -0
- docket/errors.py +16 -0
- docket/models/__init__.py +221 -0
- docket/models/access_role.py +12 -0
- docket/models/access_role_nullable_type_1.py +12 -0
- docket/models/access_role_nullable_type_2_type_1.py +12 -0
- docket/models/access_role_nullable_type_3_type_1.py +12 -0
- docket/models/acquisition_variable.py +72 -0
- docket/models/acquisition_warning.py +126 -0
- docket/models/add_principal_request.py +83 -0
- docket/models/api_token_response.py +271 -0
- docket/models/api_token_status.py +10 -0
- docket/models/archive_recurring_acquisition_request.py +77 -0
- docket/models/archive_recurring_acquisition_response.py +83 -0
- docket/models/archive_scheduled_acquisition_request.py +78 -0
- docket/models/archive_template_request.py +66 -0
- docket/models/auth_mode.py +10 -0
- docket/models/behavior_curriculum_push_settings_request.py +85 -0
- docket/models/behavior_curriculum_settings.py +55 -0
- docket/models/behavior_curriculum_settings_preview_response.py +102 -0
- docket/models/behavior_curriculum_settings_preview_response_promoted.py +50 -0
- docket/models/behavior_curriculum_settings_version_response.py +160 -0
- docket/models/behavior_curriculum_settings_version_response_promoted.py +50 -0
- docket/models/caller_kind.py +11 -0
- docket/models/create_api_token_request.py +97 -0
- docket/models/create_application_principal_request.py +65 -0
- docket/models/create_personal_api_token_request.py +167 -0
- docket/models/create_recurring_acquisition_request.py +269 -0
- docket/models/create_role_assignment_request.py +99 -0
- docket/models/create_scheduled_acquisition_request.py +265 -0
- docket/models/create_template_request.py +323 -0
- docket/models/created_api_token_response.py +82 -0
- docket/models/effective_status.py +15 -0
- docket/models/fip_color.py +13 -0
- docket/models/fip_fiber.py +93 -0
- docket/models/fip_mode.py +9 -0
- docket/models/fip_push_settings_request.py +82 -0
- docket/models/fip_settings.py +91 -0
- docket/models/fip_settings_preview_response.py +97 -0
- docket/models/fip_settings_preview_response_promoted.py +50 -0
- docket/models/fip_settings_version_response.py +155 -0
- docket/models/fip_settings_version_response_promoted.py +50 -0
- docket/models/future_occurrences.py +9 -0
- docket/models/get_resolved_settings_response_200.py +65 -0
- docket/models/held_role_response.py +116 -0
- docket/models/http_validation_problem_details.py +204 -0
- docket/models/http_validation_problem_details_errors.py +59 -0
- docket/models/instrument_response.py +91 -0
- docket/models/kept_occurrence.py +85 -0
- docket/models/kept_occurrence_reason.py +10 -0
- docket/models/list_active_acquisitions_effective_status_item.py +15 -0
- docket/models/list_scheduled_acquisitions_planning_status.py +11 -0
- docket/models/me_caller.py +118 -0
- docket/models/me_permission.py +98 -0
- docket/models/me_response.py +158 -0
- docket/models/me_role.py +75 -0
- docket/models/occurrence_preview.py +121 -0
- docket/models/occurrence_summary.py +126 -0
- docket/models/occurrence_warnings.py +91 -0
- docket/models/permission_action.py +12 -0
- docket/models/person_assignment.py +74 -0
- docket/models/person_lookup_response.py +131 -0
- docket/models/person_response.py +103 -0
- docket/models/person_role.py +9 -0
- docket/models/personal_token_limits.py +92 -0
- docket/models/planning_status.py +11 -0
- docket/models/principal_kind.py +9 -0
- docket/models/principal_response.py +216 -0
- docket/models/problem_details.py +177 -0
- docket/models/project_response.py +75 -0
- docket/models/recurrence_exception_kind.py +13 -0
- docket/models/recurrence_exception_response.py +103 -0
- docket/models/recurrence_frequency.py +9 -0
- docket/models/recurrence_preview_response.py +101 -0
- docket/models/recurrence_rule.py +207 -0
- docket/models/recurring_acquisition_created_response.py +123 -0
- docket/models/recurring_acquisition_detail_response.py +105 -0
- docket/models/recurring_acquisition_response.py +326 -0
- docket/models/report_run_outcome_request.py +89 -0
- docket/models/resolution_mode.py +10 -0
- docket/models/resolved_settings.py +73 -0
- docket/models/role_assignment_response.py +157 -0
- docket/models/role_origin.py +10 -0
- docket/models/role_source_kind.py +10 -0
- docket/models/run_origin.py +9 -0
- docket/models/run_outcome.py +9 -0
- docket/models/run_response.py +181 -0
- docket/models/run_settings_snapshot.py +72 -0
- docket/models/run_start_response.py +91 -0
- docket/models/run_start_response_settings.py +65 -0
- docket/models/run_status.py +11 -0
- docket/models/scheduled_acquisition_edit_preview_response.py +150 -0
- docket/models/scheduled_acquisition_response.py +366 -0
- docket/models/set_planning_status_request.py +90 -0
- docket/models/settings_selector.py +138 -0
- docket/models/skipped_occurrence.py +85 -0
- docket/models/split_preview_response.py +129 -0
- docket/models/split_recurring_acquisition_request.py +261 -0
- docket/models/split_recurring_acquisition_response.py +159 -0
- docket/models/start_run_request.py +73 -0
- docket/models/subject_lookup_response.py +149 -0
- docket/models/subject_response.py +128 -0
- docket/models/sync_counts.py +80 -0
- docket/models/sync_response.py +81 -0
- docket/models/template_response.py +302 -0
- docket/models/update_scheduled_acquisition_request.py +235 -0
- docket/models/update_template_request.py +279 -0
- docket/models/weekday.py +14 -0
- docket/types.py +54 -0
- docket_auth/__init__.py +239 -0
- docket_client-0.3.2.dist-info/METADATA +233 -0
- docket_client-0.3.2.dist-info/RECORD +188 -0
- docket_client-0.3.2.dist-info/WHEEL +4 -0
- docket_client-0.3.2.dist-info/entry_points.txt +2 -0
- docket_mcp/__init__.py +6 -0
- docket_mcp/api.py +207 -0
- docket_mcp/config.py +57 -0
- docket_mcp/server.py +78 -0
- docket_mcp/signin.py +155 -0
- docket_mcp/slots.py +80 -0
- docket_mcp/times.py +160 -0
- docket_mcp/tools.py +1202 -0
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Mapping
|
|
4
|
+
from typing import Any, TypeVar, BinaryIO, TextIO, TYPE_CHECKING, Generator
|
|
5
|
+
|
|
6
|
+
from attrs import define as _attrs_define
|
|
7
|
+
from attrs import field as _attrs_field
|
|
8
|
+
|
|
9
|
+
from ..types import UNSET, Unset
|
|
10
|
+
|
|
11
|
+
from ..types import UNSET, Unset
|
|
12
|
+
from typing import cast
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from ..models.acquisition_variable import AcquisitionVariable
|
|
16
|
+
from ..models.person_assignment import PersonAssignment
|
|
17
|
+
from ..models.settings_selector import SettingsSelector
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
T = TypeVar("T", bound="UpdateTemplateRequest")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@_attrs_define
|
|
24
|
+
class UpdateTemplateRequest:
|
|
25
|
+
"""Change a template. Every field except `expected_version` is optional: an
|
|
26
|
+
omitted field is left unchanged. For the scalar defaults (`subject_id`,
|
|
27
|
+
`instrument_id`, `project_id`, `operator_instructions`,
|
|
28
|
+
`duration_minutes`, `local_start_time`) an explicit `null` clears the
|
|
29
|
+
default. `name`, `people`, `type_refs` and `variables` cannot be
|
|
30
|
+
null; send an empty list to clear a list. Lists replace the whole list when present.
|
|
31
|
+
|
|
32
|
+
Attributes:
|
|
33
|
+
expected_version (int): The `version` the client last read. A stale value is rejected with 409
|
|
34
|
+
`version_conflict`.
|
|
35
|
+
name (str | Unset): New name; unique among non-archived templates.
|
|
36
|
+
subject_id (None | str | Unset): New default subject; `null` clears it.
|
|
37
|
+
instrument_id (None | str | Unset): New default instrument; `null` clears it.
|
|
38
|
+
project_id (None | str | Unset): New default project; `null` clears it.
|
|
39
|
+
operator_instructions (None | str | Unset): New default operator text; `null` clears it.
|
|
40
|
+
people (list[PersonAssignment] | Unset): Replaces the default people.
|
|
41
|
+
type_refs (list[SettingsSelector] | Unset): Replaces the default type refs.
|
|
42
|
+
variables (list[AcquisitionVariable] | Unset): Replaces the default variables.
|
|
43
|
+
duration_minutes (int | None | Unset): New default duration in minutes; `null` clears it.
|
|
44
|
+
local_start_time (None | str | Unset):
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
expected_version: int
|
|
48
|
+
name: str | Unset = UNSET
|
|
49
|
+
subject_id: None | str | Unset = UNSET
|
|
50
|
+
instrument_id: None | str | Unset = UNSET
|
|
51
|
+
project_id: None | str | Unset = UNSET
|
|
52
|
+
operator_instructions: None | str | Unset = UNSET
|
|
53
|
+
people: list[PersonAssignment] | Unset = UNSET
|
|
54
|
+
type_refs: list[SettingsSelector] | Unset = UNSET
|
|
55
|
+
variables: list[AcquisitionVariable] | Unset = UNSET
|
|
56
|
+
duration_minutes: int | None | Unset = UNSET
|
|
57
|
+
local_start_time: None | str | Unset = UNSET
|
|
58
|
+
additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
|
|
59
|
+
|
|
60
|
+
def to_dict(self) -> dict[str, Any]:
|
|
61
|
+
from ..models.acquisition_variable import AcquisitionVariable
|
|
62
|
+
from ..models.person_assignment import PersonAssignment
|
|
63
|
+
from ..models.settings_selector import SettingsSelector
|
|
64
|
+
|
|
65
|
+
expected_version = self.expected_version
|
|
66
|
+
|
|
67
|
+
name = self.name
|
|
68
|
+
|
|
69
|
+
subject_id: None | str | Unset
|
|
70
|
+
if isinstance(self.subject_id, Unset):
|
|
71
|
+
subject_id = UNSET
|
|
72
|
+
else:
|
|
73
|
+
subject_id = self.subject_id
|
|
74
|
+
|
|
75
|
+
instrument_id: None | str | Unset
|
|
76
|
+
if isinstance(self.instrument_id, Unset):
|
|
77
|
+
instrument_id = UNSET
|
|
78
|
+
else:
|
|
79
|
+
instrument_id = self.instrument_id
|
|
80
|
+
|
|
81
|
+
project_id: None | str | Unset
|
|
82
|
+
if isinstance(self.project_id, Unset):
|
|
83
|
+
project_id = UNSET
|
|
84
|
+
else:
|
|
85
|
+
project_id = self.project_id
|
|
86
|
+
|
|
87
|
+
operator_instructions: None | str | Unset
|
|
88
|
+
if isinstance(self.operator_instructions, Unset):
|
|
89
|
+
operator_instructions = UNSET
|
|
90
|
+
else:
|
|
91
|
+
operator_instructions = self.operator_instructions
|
|
92
|
+
|
|
93
|
+
people: list[dict[str, Any]] | Unset = UNSET
|
|
94
|
+
if not isinstance(self.people, Unset):
|
|
95
|
+
people = []
|
|
96
|
+
for people_item_data in self.people:
|
|
97
|
+
people_item = people_item_data.to_dict()
|
|
98
|
+
people.append(people_item)
|
|
99
|
+
|
|
100
|
+
type_refs: list[dict[str, Any]] | Unset = UNSET
|
|
101
|
+
if not isinstance(self.type_refs, Unset):
|
|
102
|
+
type_refs = []
|
|
103
|
+
for type_refs_item_data in self.type_refs:
|
|
104
|
+
type_refs_item = type_refs_item_data.to_dict()
|
|
105
|
+
type_refs.append(type_refs_item)
|
|
106
|
+
|
|
107
|
+
variables: list[dict[str, Any]] | Unset = UNSET
|
|
108
|
+
if not isinstance(self.variables, Unset):
|
|
109
|
+
variables = []
|
|
110
|
+
for variables_item_data in self.variables:
|
|
111
|
+
variables_item = variables_item_data.to_dict()
|
|
112
|
+
variables.append(variables_item)
|
|
113
|
+
|
|
114
|
+
duration_minutes: int | None | Unset
|
|
115
|
+
if isinstance(self.duration_minutes, Unset):
|
|
116
|
+
duration_minutes = UNSET
|
|
117
|
+
else:
|
|
118
|
+
duration_minutes = self.duration_minutes
|
|
119
|
+
|
|
120
|
+
local_start_time: None | str | Unset
|
|
121
|
+
if isinstance(self.local_start_time, Unset):
|
|
122
|
+
local_start_time = UNSET
|
|
123
|
+
else:
|
|
124
|
+
local_start_time = self.local_start_time
|
|
125
|
+
|
|
126
|
+
field_dict: dict[str, Any] = {}
|
|
127
|
+
field_dict.update(self.additional_properties)
|
|
128
|
+
field_dict.update(
|
|
129
|
+
{
|
|
130
|
+
"expected_version": expected_version,
|
|
131
|
+
}
|
|
132
|
+
)
|
|
133
|
+
if name is not UNSET:
|
|
134
|
+
field_dict["name"] = name
|
|
135
|
+
if subject_id is not UNSET:
|
|
136
|
+
field_dict["subject_id"] = subject_id
|
|
137
|
+
if instrument_id is not UNSET:
|
|
138
|
+
field_dict["instrument_id"] = instrument_id
|
|
139
|
+
if project_id is not UNSET:
|
|
140
|
+
field_dict["project_id"] = project_id
|
|
141
|
+
if operator_instructions is not UNSET:
|
|
142
|
+
field_dict["operator_instructions"] = operator_instructions
|
|
143
|
+
if people is not UNSET:
|
|
144
|
+
field_dict["people"] = people
|
|
145
|
+
if type_refs is not UNSET:
|
|
146
|
+
field_dict["type_refs"] = type_refs
|
|
147
|
+
if variables is not UNSET:
|
|
148
|
+
field_dict["variables"] = variables
|
|
149
|
+
if duration_minutes is not UNSET:
|
|
150
|
+
field_dict["duration_minutes"] = duration_minutes
|
|
151
|
+
if local_start_time is not UNSET:
|
|
152
|
+
field_dict["local_start_time"] = local_start_time
|
|
153
|
+
|
|
154
|
+
return field_dict
|
|
155
|
+
|
|
156
|
+
@classmethod
|
|
157
|
+
def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T:
|
|
158
|
+
from ..models.acquisition_variable import AcquisitionVariable
|
|
159
|
+
from ..models.person_assignment import PersonAssignment
|
|
160
|
+
from ..models.settings_selector import SettingsSelector
|
|
161
|
+
|
|
162
|
+
d = dict(src_dict)
|
|
163
|
+
expected_version = d.pop("expected_version")
|
|
164
|
+
|
|
165
|
+
name = d.pop("name", UNSET)
|
|
166
|
+
|
|
167
|
+
def _parse_subject_id(data: object) -> None | str | Unset:
|
|
168
|
+
if data is None:
|
|
169
|
+
return data
|
|
170
|
+
if isinstance(data, Unset):
|
|
171
|
+
return data
|
|
172
|
+
return cast(None | str | Unset, data)
|
|
173
|
+
|
|
174
|
+
subject_id = _parse_subject_id(d.pop("subject_id", UNSET))
|
|
175
|
+
|
|
176
|
+
def _parse_instrument_id(data: object) -> None | str | Unset:
|
|
177
|
+
if data is None:
|
|
178
|
+
return data
|
|
179
|
+
if isinstance(data, Unset):
|
|
180
|
+
return data
|
|
181
|
+
return cast(None | str | Unset, data)
|
|
182
|
+
|
|
183
|
+
instrument_id = _parse_instrument_id(d.pop("instrument_id", UNSET))
|
|
184
|
+
|
|
185
|
+
def _parse_project_id(data: object) -> None | str | Unset:
|
|
186
|
+
if data is None:
|
|
187
|
+
return data
|
|
188
|
+
if isinstance(data, Unset):
|
|
189
|
+
return data
|
|
190
|
+
return cast(None | str | Unset, data)
|
|
191
|
+
|
|
192
|
+
project_id = _parse_project_id(d.pop("project_id", UNSET))
|
|
193
|
+
|
|
194
|
+
def _parse_operator_instructions(data: object) -> None | str | Unset:
|
|
195
|
+
if data is None:
|
|
196
|
+
return data
|
|
197
|
+
if isinstance(data, Unset):
|
|
198
|
+
return data
|
|
199
|
+
return cast(None | str | Unset, data)
|
|
200
|
+
|
|
201
|
+
operator_instructions = _parse_operator_instructions(d.pop("operator_instructions", UNSET))
|
|
202
|
+
|
|
203
|
+
_people = d.pop("people", UNSET)
|
|
204
|
+
people: list[PersonAssignment] | Unset = UNSET
|
|
205
|
+
if _people is not UNSET:
|
|
206
|
+
people = []
|
|
207
|
+
for people_item_data in _people:
|
|
208
|
+
people_item = PersonAssignment.from_dict(people_item_data)
|
|
209
|
+
|
|
210
|
+
people.append(people_item)
|
|
211
|
+
|
|
212
|
+
_type_refs = d.pop("type_refs", UNSET)
|
|
213
|
+
type_refs: list[SettingsSelector] | Unset = UNSET
|
|
214
|
+
if _type_refs is not UNSET:
|
|
215
|
+
type_refs = []
|
|
216
|
+
for type_refs_item_data in _type_refs:
|
|
217
|
+
type_refs_item = SettingsSelector.from_dict(type_refs_item_data)
|
|
218
|
+
|
|
219
|
+
type_refs.append(type_refs_item)
|
|
220
|
+
|
|
221
|
+
_variables = d.pop("variables", UNSET)
|
|
222
|
+
variables: list[AcquisitionVariable] | Unset = UNSET
|
|
223
|
+
if _variables is not UNSET:
|
|
224
|
+
variables = []
|
|
225
|
+
for variables_item_data in _variables:
|
|
226
|
+
variables_item = AcquisitionVariable.from_dict(variables_item_data)
|
|
227
|
+
|
|
228
|
+
variables.append(variables_item)
|
|
229
|
+
|
|
230
|
+
def _parse_duration_minutes(data: object) -> int | None | Unset:
|
|
231
|
+
if data is None:
|
|
232
|
+
return data
|
|
233
|
+
if isinstance(data, Unset):
|
|
234
|
+
return data
|
|
235
|
+
return cast(int | None | Unset, data)
|
|
236
|
+
|
|
237
|
+
duration_minutes = _parse_duration_minutes(d.pop("duration_minutes", UNSET))
|
|
238
|
+
|
|
239
|
+
def _parse_local_start_time(data: object) -> None | str | Unset:
|
|
240
|
+
if data is None:
|
|
241
|
+
return data
|
|
242
|
+
if isinstance(data, Unset):
|
|
243
|
+
return data
|
|
244
|
+
return cast(None | str | Unset, data)
|
|
245
|
+
|
|
246
|
+
local_start_time = _parse_local_start_time(d.pop("local_start_time", UNSET))
|
|
247
|
+
|
|
248
|
+
update_template_request = cls(
|
|
249
|
+
expected_version=expected_version,
|
|
250
|
+
name=name,
|
|
251
|
+
subject_id=subject_id,
|
|
252
|
+
instrument_id=instrument_id,
|
|
253
|
+
project_id=project_id,
|
|
254
|
+
operator_instructions=operator_instructions,
|
|
255
|
+
people=people,
|
|
256
|
+
type_refs=type_refs,
|
|
257
|
+
variables=variables,
|
|
258
|
+
duration_minutes=duration_minutes,
|
|
259
|
+
local_start_time=local_start_time,
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
update_template_request.additional_properties = d
|
|
263
|
+
return update_template_request
|
|
264
|
+
|
|
265
|
+
@property
|
|
266
|
+
def additional_keys(self) -> list[str]:
|
|
267
|
+
return list(self.additional_properties.keys())
|
|
268
|
+
|
|
269
|
+
def __getitem__(self, key: str) -> Any:
|
|
270
|
+
return self.additional_properties[key]
|
|
271
|
+
|
|
272
|
+
def __setitem__(self, key: str, value: Any) -> None:
|
|
273
|
+
self.additional_properties[key] = value
|
|
274
|
+
|
|
275
|
+
def __delitem__(self, key: str) -> None:
|
|
276
|
+
del self.additional_properties[key]
|
|
277
|
+
|
|
278
|
+
def __contains__(self, key: str) -> bool:
|
|
279
|
+
return key in self.additional_properties
|
docket/models/weekday.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Weekday(str, Enum):
|
|
5
|
+
FRIDAY = "friday"
|
|
6
|
+
MONDAY = "monday"
|
|
7
|
+
SATURDAY = "saturday"
|
|
8
|
+
SUNDAY = "sunday"
|
|
9
|
+
THURSDAY = "thursday"
|
|
10
|
+
TUESDAY = "tuesday"
|
|
11
|
+
WEDNESDAY = "wednesday"
|
|
12
|
+
|
|
13
|
+
def __str__(self) -> str:
|
|
14
|
+
return str(self.value)
|
docket/types.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""Contains some shared types for properties"""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Mapping, MutableMapping
|
|
4
|
+
from http import HTTPStatus
|
|
5
|
+
from typing import BinaryIO, Generic, TypeVar, Literal, IO
|
|
6
|
+
|
|
7
|
+
from attrs import define
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Unset:
|
|
11
|
+
def __bool__(self) -> Literal[False]:
|
|
12
|
+
return False
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
UNSET: Unset = Unset()
|
|
16
|
+
|
|
17
|
+
# The types that `httpx.Client(files=)` can accept, copied from that library.
|
|
18
|
+
FileContent = IO[bytes] | bytes | str
|
|
19
|
+
FileTypes = (
|
|
20
|
+
# (filename, file (or bytes), content_type)
|
|
21
|
+
tuple[str | None, FileContent, str | None]
|
|
22
|
+
# (filename, file (or bytes), content_type, headers)
|
|
23
|
+
| tuple[str | None, FileContent, str | None, Mapping[str, str]]
|
|
24
|
+
)
|
|
25
|
+
RequestFiles = list[tuple[str, FileTypes]]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@define
|
|
29
|
+
class File:
|
|
30
|
+
"""Contains information for file uploads"""
|
|
31
|
+
|
|
32
|
+
payload: BinaryIO
|
|
33
|
+
file_name: str | None = None
|
|
34
|
+
mime_type: str | None = None
|
|
35
|
+
|
|
36
|
+
def to_tuple(self) -> FileTypes:
|
|
37
|
+
"""Return a tuple representation that httpx will accept for multipart/form-data"""
|
|
38
|
+
return self.file_name, self.payload, self.mime_type
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
T = TypeVar("T")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@define
|
|
45
|
+
class Response(Generic[T]):
|
|
46
|
+
"""A response from an endpoint"""
|
|
47
|
+
|
|
48
|
+
status_code: HTTPStatus
|
|
49
|
+
content: bytes
|
|
50
|
+
headers: MutableMapping[str, str]
|
|
51
|
+
parsed: T | None
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
__all__ = ["UNSET", "File", "FileTypes", "RequestFiles", "Response", "Unset"]
|
docket_auth/__init__.py
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
"""Bearer-token providers for the docket client.
|
|
2
|
+
|
|
3
|
+
Hand-written; ``client/docket/`` is generated and never edited. A *token
|
|
4
|
+
provider* is any zero-argument callable returning a current access token.
|
|
5
|
+
[`authenticated_client`][docket_auth.authenticated_client] asks it before every request, so tokens refresh
|
|
6
|
+
without rebuilding the client.
|
|
7
|
+
|
|
8
|
+
Two providers use ``msal`` (install ``docket-client[auth]``):
|
|
9
|
+
|
|
10
|
+
* [`ClientCredentialsTokenProvider`][docket_auth.ClientCredentialsTokenProvider] - rigs, clabe and services: a
|
|
11
|
+
confidential client signing in as itself (Entra app role ``Docket.Machine``).
|
|
12
|
+
* [`DeviceCodeTokenProvider`][docket_auth.DeviceCodeTokenProvider] - people: device-code sign-in in a terminal,
|
|
13
|
+
with a token cache so the code is needed once.
|
|
14
|
+
|
|
15
|
+
Reads need no token; only mutations do. Run tokens (``/runs/{token}``) identify
|
|
16
|
+
a run and grant no authority.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import os
|
|
22
|
+
import sys
|
|
23
|
+
from collections.abc import Callable, Generator
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
from typing import Any
|
|
26
|
+
|
|
27
|
+
import httpx
|
|
28
|
+
|
|
29
|
+
from docket import AuthenticatedClient
|
|
30
|
+
|
|
31
|
+
__all__ = [
|
|
32
|
+
"ClientCredentialsTokenProvider",
|
|
33
|
+
"api_token_provider",
|
|
34
|
+
"DeviceCodeTokenProvider",
|
|
35
|
+
"TokenProvider",
|
|
36
|
+
"authenticated_client",
|
|
37
|
+
"static_provider",
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
TokenProvider = Callable[[], str]
|
|
41
|
+
"""Returns a valid access token; called before each request."""
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class AuthenticationError(RuntimeError):
|
|
45
|
+
"""Raised when a token cannot be acquired."""
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _authority(tenant_id: str | None, authority: str | None) -> tuple[str, bool]:
|
|
49
|
+
"""Returns (authority URL, is_generic_oidc). Entra by tenant id, or any OIDC authority."""
|
|
50
|
+
if authority:
|
|
51
|
+
return authority.rstrip("/"), True
|
|
52
|
+
if not tenant_id:
|
|
53
|
+
raise ValueError("Pass tenant_id (Microsoft Entra) or authority (another OIDC provider).")
|
|
54
|
+
return f"https://login.microsoftonline.com/{tenant_id}", False
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _msal_kwargs(tenant_id: str | None, authority: str | None) -> dict[str, Any]:
|
|
58
|
+
url, generic = _authority(tenant_id, authority)
|
|
59
|
+
return {"oidc_authority": url} if generic else {"authority": url}
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _result_token(result: dict[str, Any] | None) -> str:
|
|
63
|
+
result = result or {}
|
|
64
|
+
token = result.get("access_token")
|
|
65
|
+
if not token:
|
|
66
|
+
# error / error_description never contain the secret or a token.
|
|
67
|
+
raise AuthenticationError(
|
|
68
|
+
f"Could not get a token: {result.get('error')}: {result.get('error_description')}"
|
|
69
|
+
)
|
|
70
|
+
return str(token)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class ClientCredentialsTokenProvider:
|
|
74
|
+
"""Signs in as an application (OAuth2 client credentials) with ``msal``.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
client_id: The application (client) id of the rig's app registration.
|
|
78
|
+
client_secret: Its client secret. Keep it out of source control.
|
|
79
|
+
scope: What to request, normally ``"<API client id or app id URI>/.default"``.
|
|
80
|
+
tenant_id: The Entra tenant id. Omit when ``authority`` is given.
|
|
81
|
+
authority: Another OIDC authority (the dev mock server), e.g. ``http://localhost:8003/docket``.
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
def __init__(
|
|
85
|
+
self,
|
|
86
|
+
client_id: str,
|
|
87
|
+
client_secret: str,
|
|
88
|
+
scope: str,
|
|
89
|
+
*,
|
|
90
|
+
tenant_id: str | None = None,
|
|
91
|
+
authority: str | None = None,
|
|
92
|
+
) -> None:
|
|
93
|
+
import msal
|
|
94
|
+
|
|
95
|
+
self._scopes = [scope]
|
|
96
|
+
self._app = msal.ConfidentialClientApplication(
|
|
97
|
+
client_id,
|
|
98
|
+
client_credential=client_secret,
|
|
99
|
+
**_msal_kwargs(tenant_id, authority),
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
def __call__(self) -> str:
|
|
103
|
+
# msal serves the cached token until shortly before it expires.
|
|
104
|
+
return _result_token(self._app.acquire_token_for_client(scopes=self._scopes))
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class DeviceCodeTokenProvider:
|
|
108
|
+
"""Signs a person in with the device-code flow (``msal`` public client).
|
|
109
|
+
|
|
110
|
+
The first call prints a URL and a code to open in any browser; after that
|
|
111
|
+
the cached token and its refresh token are used. Pass ``cache_path`` to keep
|
|
112
|
+
the cache between runs (it holds refresh tokens: protect the file).
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
client_id: The SPA / public-client application id registered for this API.
|
|
116
|
+
scope: Normally ``"<API client id or app id URI>/access_as_user"``.
|
|
117
|
+
tenant_id: The Entra tenant id. Omit when ``authority`` is given.
|
|
118
|
+
authority: Another OIDC authority (the dev mock server).
|
|
119
|
+
cache_path: File to persist the token cache in. Omit for in-memory.
|
|
120
|
+
prompt: Called with the sign-in instructions; defaults to printing them.
|
|
121
|
+
"""
|
|
122
|
+
|
|
123
|
+
def __init__(
|
|
124
|
+
self,
|
|
125
|
+
client_id: str,
|
|
126
|
+
scope: str,
|
|
127
|
+
*,
|
|
128
|
+
tenant_id: str | None = None,
|
|
129
|
+
authority: str | None = None,
|
|
130
|
+
cache_path: str | Path | None = None,
|
|
131
|
+
prompt: Callable[[str], None] | None = None,
|
|
132
|
+
) -> None:
|
|
133
|
+
import msal
|
|
134
|
+
|
|
135
|
+
self._scopes = [scope]
|
|
136
|
+
self._prompt = prompt or (lambda message: print(message, file=sys.stderr))
|
|
137
|
+
self._cache_path = Path(cache_path).expanduser() if cache_path else None
|
|
138
|
+
self._cache = msal.SerializableTokenCache()
|
|
139
|
+
if self._cache_path and self._cache_path.exists():
|
|
140
|
+
self._cache.deserialize(self._cache_path.read_text(encoding="utf-8"))
|
|
141
|
+
self._app = msal.PublicClientApplication(
|
|
142
|
+
client_id, token_cache=self._cache, **_msal_kwargs(tenant_id, authority)
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
def __call__(self) -> str:
|
|
146
|
+
for account in self._app.get_accounts():
|
|
147
|
+
result = self._app.acquire_token_silent(self._scopes, account=account)
|
|
148
|
+
if result and result.get("access_token"):
|
|
149
|
+
self._save()
|
|
150
|
+
return str(result["access_token"])
|
|
151
|
+
|
|
152
|
+
flow = self._app.initiate_device_flow(scopes=self._scopes)
|
|
153
|
+
if "user_code" not in flow:
|
|
154
|
+
raise AuthenticationError(
|
|
155
|
+
f"Could not start device sign-in: {flow.get('error_description')}"
|
|
156
|
+
)
|
|
157
|
+
self._prompt(str(flow["message"]))
|
|
158
|
+
token = _result_token(self._app.acquire_token_by_device_flow(flow))
|
|
159
|
+
self._save()
|
|
160
|
+
return token
|
|
161
|
+
|
|
162
|
+
def silent_token(self) -> str | None:
|
|
163
|
+
"""A cached token if one can be had without any interaction, else None (never signs in)."""
|
|
164
|
+
for account in self._app.get_accounts():
|
|
165
|
+
result = self._app.acquire_token_silent(self._scopes, account=account)
|
|
166
|
+
if result and result.get("access_token"):
|
|
167
|
+
self._save()
|
|
168
|
+
return str(result["access_token"])
|
|
169
|
+
return None
|
|
170
|
+
|
|
171
|
+
def _save(self) -> None:
|
|
172
|
+
if self._cache_path and self._cache.has_state_changed:
|
|
173
|
+
# It holds refresh tokens: created owner-only (the mode is ignored on Windows,
|
|
174
|
+
# where the user profile's ACLs apply).
|
|
175
|
+
self._cache_path.parent.mkdir(parents=True, exist_ok=True)
|
|
176
|
+
descriptor = os.open(self._cache_path, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
|
|
177
|
+
with os.fdopen(descriptor, "w", encoding="utf-8") as cache_file:
|
|
178
|
+
cache_file.write(self._cache.serialize())
|
|
179
|
+
if os.name == "posix":
|
|
180
|
+
self._cache_path.chmod(0o600)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
class _ProviderAuth(httpx.Auth):
|
|
184
|
+
"""Sets ``Authorization: Bearer <token>`` from the provider on every request."""
|
|
185
|
+
|
|
186
|
+
def __init__(self, provider: TokenProvider) -> None:
|
|
187
|
+
self._provider = provider
|
|
188
|
+
|
|
189
|
+
def auth_flow(self, request: httpx.Request) -> Generator[httpx.Request, httpx.Response, None]:
|
|
190
|
+
request.headers["Authorization"] = f"Bearer {self._provider()}"
|
|
191
|
+
yield request
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def authenticated_client(
|
|
195
|
+
base_url: str,
|
|
196
|
+
token_provider: TokenProvider,
|
|
197
|
+
*,
|
|
198
|
+
verify_ssl: bool | str = True,
|
|
199
|
+
**kwargs: Any,
|
|
200
|
+
) -> AuthenticatedClient:
|
|
201
|
+
"""An [`docket.AuthenticatedClient`][docket.client.AuthenticatedClient] that gets its token from ``token_provider``.
|
|
202
|
+
|
|
203
|
+
Extra keyword arguments (``timeout``, ``headers``, ...) go to the client.
|
|
204
|
+
The provider is called for every request, so rotation and refresh need no
|
|
205
|
+
special handling.
|
|
206
|
+
"""
|
|
207
|
+
httpx_args = dict(kwargs.pop("httpx_args", {}))
|
|
208
|
+
httpx_args["auth"] = _ProviderAuth(token_provider)
|
|
209
|
+
return AuthenticatedClient(
|
|
210
|
+
base_url=base_url,
|
|
211
|
+
token="provider", # placeholder; replaced per request by _ProviderAuth
|
|
212
|
+
verify_ssl=verify_ssl,
|
|
213
|
+
httpx_args=httpx_args,
|
|
214
|
+
**kwargs,
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def api_token_provider(variable: str = "DOCKET_API_TOKEN") -> TokenProvider:
|
|
219
|
+
"""A provider for a Docket-issued API token (``dkt_...``) read from the environment.
|
|
220
|
+
|
|
221
|
+
No Entra registration is involved: an admin issues the token in Docket's Access page and
|
|
222
|
+
it is set as ``DOCKET_API_TOKEN`` on the rig or script host. The variable is read on every
|
|
223
|
+
request, so a rotated token is picked up without a restart. The token is never printed.
|
|
224
|
+
"""
|
|
225
|
+
|
|
226
|
+
def provider() -> str:
|
|
227
|
+
token = os.environ.get(variable, "").strip()
|
|
228
|
+
if not token:
|
|
229
|
+
raise AuthenticationError(
|
|
230
|
+
f"Set {variable} to a Docket API token (issued by a Docket admin)."
|
|
231
|
+
)
|
|
232
|
+
return token
|
|
233
|
+
|
|
234
|
+
return provider
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def static_provider(token: str) -> TokenProvider:
|
|
238
|
+
"""A provider that always returns ``token`` (tests, or a token you obtained elsewhere)."""
|
|
239
|
+
return lambda: token
|