targetprocess-py 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.
- targetprocess/__init__.py +231 -0
- targetprocess/_assignables.py +162 -0
- targetprocess/_base.py +318 -0
- targetprocess/_content.py +287 -0
- targetprocess/_dates.py +70 -0
- targetprocess/_entity_types.py +33 -0
- targetprocess/_generals.py +324 -0
- targetprocess/_joins.py +217 -0
- targetprocess/_lookups.py +477 -0
- targetprocess/_nested.py +223 -0
- targetprocess/_observability.py +368 -0
- targetprocess/client.py +430 -0
- targetprocess/exceptions.py +147 -0
- targetprocess/models.py +121 -0
- targetprocess/py.typed +1 -0
- targetprocess/request_handler.py +724 -0
- targetprocess/resources/__init__.py +76 -0
- targetprocess/resources/assignments.py +29 -0
- targetprocess/resources/attachments.py +164 -0
- targetprocess/resources/base.py +563 -0
- targetprocess/resources/bugs.py +20 -0
- targetprocess/resources/comments.py +23 -0
- targetprocess/resources/custom_activities.py +56 -0
- targetprocess/resources/custom_fields.py +27 -0
- targetprocess/resources/custom_rules.py +28 -0
- targetprocess/resources/entities.py +433 -0
- targetprocess/resources/entity_states.py +20 -0
- targetprocess/resources/entity_types.py +61 -0
- targetprocess/resources/epics.py +20 -0
- targetprocess/resources/features.py +20 -0
- targetprocess/resources/iterations.py +20 -0
- targetprocess/resources/priorities.py +85 -0
- targetprocess/resources/processes.py +65 -0
- targetprocess/resources/projects.py +20 -0
- targetprocess/resources/relation_types.py +59 -0
- targetprocess/resources/relations.py +34 -0
- targetprocess/resources/releases.py +20 -0
- targetprocess/resources/requests.py +20 -0
- targetprocess/resources/role_efforts.py +21 -0
- targetprocess/resources/roles.py +52 -0
- targetprocess/resources/severities.py +53 -0
- targetprocess/resources/tasks.py +20 -0
- targetprocess/resources/team_assignments.py +21 -0
- targetprocess/resources/team_iterations.py +24 -0
- targetprocess/resources/teams.py +20 -0
- targetprocess/resources/terms.py +28 -0
- targetprocess/resources/test_cases.py +20 -0
- targetprocess/resources/times.py +392 -0
- targetprocess/resources/user_stories.py +20 -0
- targetprocess/resources/users.py +20 -0
- targetprocess/resources/workflows.py +27 -0
- targetprocess/response_parser.py +113 -0
- targetprocess/transport.py +116 -0
- targetprocess/types.py +56 -0
- targetprocess_py-0.1.0.dist-info/METADATA +476 -0
- targetprocess_py-0.1.0.dist-info/RECORD +58 -0
- targetprocess_py-0.1.0.dist-info/WHEEL +4 -0
- targetprocess_py-0.1.0.dist-info/licenses/LICENSE +21 -0
targetprocess/_base.py
ADDED
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
"""Base entity classes, mirroring TargetProcess's own type hierarchy.
|
|
2
|
+
|
|
3
|
+
TP's ``/meta`` declares a real inheritance chain: most domain types derive from
|
|
4
|
+
``General``, and the work-item types derive from ``Assignable``, which itself
|
|
5
|
+
derives from ``General``. :class:`GeneralEntity` and :class:`AssignableEntity`
|
|
6
|
+
model those two bases, so every field TP defines once is declared once here
|
|
7
|
+
rather than repeated on each concrete type.
|
|
8
|
+
|
|
9
|
+
:class:`Entity` and :class:`NamedEntity` sit above them and carry no TP
|
|
10
|
+
counterpart - they hold the identity fields every payload shares and the
|
|
11
|
+
``Name`` split, so the types TP exposes outside the ``General`` chain (the
|
|
12
|
+
lookup and join types) have a base too.
|
|
13
|
+
|
|
14
|
+
The concrete entity models live in the sibling private modules and are
|
|
15
|
+
re-exported by :mod:`targetprocess.models`, which stays the import surface
|
|
16
|
+
callers use.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from datetime import datetime
|
|
20
|
+
|
|
21
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
22
|
+
|
|
23
|
+
from targetprocess._dates import TPDateTime
|
|
24
|
+
from targetprocess._nested import (
|
|
25
|
+
AssignedUsers,
|
|
26
|
+
CustomFieldValue,
|
|
27
|
+
EntityRef,
|
|
28
|
+
EntityTypeRef,
|
|
29
|
+
RefWithImportance,
|
|
30
|
+
UserRef,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class Entity(BaseModel):
|
|
35
|
+
"""Base class for all TargetProcess entities.
|
|
36
|
+
|
|
37
|
+
Provides the fields common to every TP entity. Entity types that carry no
|
|
38
|
+
``Name`` field (``User``, ``Comment``, ``Assignment``, ``TeamAssignment``,
|
|
39
|
+
``RoleEffort``, ``Relation``, ``Time``) extend this base directly; types
|
|
40
|
+
with a display name extend :class:`NamedEntity`, and the ``General`` /
|
|
41
|
+
``Assignable`` families extend the two bases below that. Pydantic aliases
|
|
42
|
+
handle the API's PascalCase format automatically.
|
|
43
|
+
|
|
44
|
+
``CreateDate`` and ``ModifyDate`` are declared here because most types
|
|
45
|
+
carry them, but TP exposes neither on every type - on a type whose
|
|
46
|
+
``/meta`` omits them (the lookup and join types, largely) they simply stay
|
|
47
|
+
``None``.
|
|
48
|
+
|
|
49
|
+
Undeclared API fields are never silently discarded: ``extra="allow"``
|
|
50
|
+
preserves them in ``model_extra`` under their wire (PascalCase) names,
|
|
51
|
+
reachable via attribute access and included by ``model_dump``. TP payload
|
|
52
|
+
shapes vary by instance and version, so undeclared keys are tolerated
|
|
53
|
+
rather than rejected - but kept, not dropped. Assigning an undeclared
|
|
54
|
+
attribute (a mistyped name included) likewise lands in ``model_extra``.
|
|
55
|
+
|
|
56
|
+
Attributes:
|
|
57
|
+
id: Unique identifier for the entity
|
|
58
|
+
resource_type: Type of the entity (e.g., "UserStory", "Bug", "User")
|
|
59
|
+
create_date: When the entity was created (optional)
|
|
60
|
+
modify_date: When the entity was last modified (optional)
|
|
61
|
+
custom_fields: Custom-field values embedded in the entity's
|
|
62
|
+
``CustomFields`` array (present when fetched via ``include=``)
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
model_config = ConfigDict(
|
|
66
|
+
populate_by_name=True, # Allow both alias and field name
|
|
67
|
+
str_strip_whitespace=True, # Strip whitespace from strings
|
|
68
|
+
validate_assignment=True, # Validate on assignment
|
|
69
|
+
extra="allow", # Preserve undeclared API fields in model_extra
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
id: int = Field(alias="Id", description="Unique identifier")
|
|
73
|
+
resource_type: str | None = Field(default=None, alias="ResourceType", description="Entity type")
|
|
74
|
+
create_date: TPDateTime | None = Field(
|
|
75
|
+
default=None, alias="CreateDate", description="Creation timestamp"
|
|
76
|
+
)
|
|
77
|
+
modify_date: TPDateTime | None = Field(
|
|
78
|
+
default=None, alias="ModifyDate", description="Modification timestamp"
|
|
79
|
+
)
|
|
80
|
+
custom_fields: list[CustomFieldValue] | None = Field(
|
|
81
|
+
default=None, alias="CustomFields", description="Custom-field values"
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
# Property accessors for PascalCase access
|
|
85
|
+
@property
|
|
86
|
+
def Id(self) -> int:
|
|
87
|
+
"""Access id field via PascalCase (API format)."""
|
|
88
|
+
return self.id
|
|
89
|
+
|
|
90
|
+
@property
|
|
91
|
+
def ResourceType(self) -> str | None:
|
|
92
|
+
"""Access resource_type field via PascalCase (API format)."""
|
|
93
|
+
return self.resource_type
|
|
94
|
+
|
|
95
|
+
@property
|
|
96
|
+
def CreateDate(self) -> datetime | None:
|
|
97
|
+
"""Access create_date field via PascalCase (API format)."""
|
|
98
|
+
return self.create_date
|
|
99
|
+
|
|
100
|
+
@property
|
|
101
|
+
def ModifyDate(self) -> datetime | None:
|
|
102
|
+
"""Access modify_date field via PascalCase (API format)."""
|
|
103
|
+
return self.modify_date
|
|
104
|
+
|
|
105
|
+
@property
|
|
106
|
+
def CustomFields(self) -> list[CustomFieldValue] | None:
|
|
107
|
+
"""Access custom_fields field via PascalCase (API format)."""
|
|
108
|
+
return self.custom_fields
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class NamedEntity(Entity):
|
|
112
|
+
"""Base class for TP entities that carry a display Name.
|
|
113
|
+
|
|
114
|
+
The common shape - most domain entities have a ``Name``. Which types do
|
|
115
|
+
not, and why, is stated once on :class:`Entity`.
|
|
116
|
+
|
|
117
|
+
Attributes:
|
|
118
|
+
name: Display name of the entity
|
|
119
|
+
"""
|
|
120
|
+
|
|
121
|
+
name: str | None = Field(default=None, alias="Name", description="Display name")
|
|
122
|
+
|
|
123
|
+
@property
|
|
124
|
+
def Name(self) -> str | None:
|
|
125
|
+
"""Access name field via PascalCase (API format)."""
|
|
126
|
+
return self.name
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class GeneralEntity(NamedEntity):
|
|
130
|
+
"""TP's ``General`` base - the shape shared by every domain entity.
|
|
131
|
+
|
|
132
|
+
``Project``, ``Team``, ``Release``, ``Iteration``, ``TeamIteration`` and
|
|
133
|
+
``TestCase`` derive from this directly; the work-item types reach it
|
|
134
|
+
through :class:`AssignableEntity`.
|
|
135
|
+
|
|
136
|
+
The four ``GeneralUser``-shaped references (``Owner``, ``Creator``,
|
|
137
|
+
``LastEditor``, ``LastCommentedUser``) arrive as :class:`UserRef` - TP
|
|
138
|
+
sends them with ``FirstName``/``LastName``/``Login``/``FullName`` and no
|
|
139
|
+
``Name`` key. ``Owner`` records who *created* the entity, not who is
|
|
140
|
+
working it: for a work item that is the ``Assignment`` collection (see
|
|
141
|
+
``client.assignments``).
|
|
142
|
+
|
|
143
|
+
Attributes:
|
|
144
|
+
description: Detailed description
|
|
145
|
+
start_date: Start date/time
|
|
146
|
+
end_date: End date/time
|
|
147
|
+
last_comment_date: When the entity was last commented on
|
|
148
|
+
tags: Comma-separated tag string (TP stores tags as one string)
|
|
149
|
+
numeric_priority: TP's global ordering rank (a float, not a Priority)
|
|
150
|
+
entity_version: Per-record version counter (increments on every edit)
|
|
151
|
+
is_now: Whether the entity sits in the current time window
|
|
152
|
+
is_next: Whether the entity sits in the next time window
|
|
153
|
+
is_previous: Whether the entity sits in the previous time window
|
|
154
|
+
entity_type: The TP entity type this record is an instance of
|
|
155
|
+
owner: Who created the entity (TP deprecates this in favour of creator)
|
|
156
|
+
creator: Who created the entity
|
|
157
|
+
last_editor: Who last modified the entity
|
|
158
|
+
last_commented_user: Who last commented on the entity
|
|
159
|
+
project: Project reference
|
|
160
|
+
linked_test_plan: Linked test plan reference
|
|
161
|
+
milestone: Milestone reference
|
|
162
|
+
"""
|
|
163
|
+
|
|
164
|
+
# Content
|
|
165
|
+
description: str | None = Field(
|
|
166
|
+
default=None, alias="Description", description="Detailed description"
|
|
167
|
+
)
|
|
168
|
+
tags: str | None = Field(default=None, alias="Tags", description="Comma-separated tags")
|
|
169
|
+
|
|
170
|
+
# Date tracking
|
|
171
|
+
start_date: TPDateTime | None = Field(
|
|
172
|
+
default=None, alias="StartDate", description="Start date/time"
|
|
173
|
+
)
|
|
174
|
+
end_date: TPDateTime | None = Field(default=None, alias="EndDate", description="End date/time")
|
|
175
|
+
last_comment_date: TPDateTime | None = Field(
|
|
176
|
+
default=None, alias="LastCommentDate", description="Last comment date/time"
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
# Ordering / versioning
|
|
180
|
+
numeric_priority: float | None = Field(
|
|
181
|
+
default=None, alias="NumericPriority", description="Global ordering rank"
|
|
182
|
+
)
|
|
183
|
+
entity_version: int | None = Field(
|
|
184
|
+
default=None, alias="EntityVersion", description="Per-record version counter"
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
# Time-window flags
|
|
188
|
+
is_now: bool | None = Field(default=None, alias="IsNow", description="In the current window")
|
|
189
|
+
is_next: bool | None = Field(default=None, alias="IsNext", description="In the next window")
|
|
190
|
+
is_previous: bool | None = Field(
|
|
191
|
+
default=None, alias="IsPrevious", description="In the previous window"
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
# Relationships
|
|
195
|
+
entity_type: EntityTypeRef | None = Field(
|
|
196
|
+
default=None, alias="EntityType", description="TP entity type"
|
|
197
|
+
)
|
|
198
|
+
owner: UserRef | None = Field(default=None, alias="Owner", description="Creator (deprecated)")
|
|
199
|
+
creator: UserRef | None = Field(default=None, alias="Creator", description="Creator")
|
|
200
|
+
last_editor: UserRef | None = Field(
|
|
201
|
+
default=None, alias="LastEditor", description="Last modifier"
|
|
202
|
+
)
|
|
203
|
+
last_commented_user: UserRef | None = Field(
|
|
204
|
+
default=None, alias="LastCommentedUser", description="Last commenter"
|
|
205
|
+
)
|
|
206
|
+
project: EntityRef | None = Field(default=None, alias="Project", description="Project")
|
|
207
|
+
linked_test_plan: EntityRef | None = Field(
|
|
208
|
+
default=None, alias="LinkedTestPlan", description="Linked test plan"
|
|
209
|
+
)
|
|
210
|
+
milestone: EntityRef | None = Field(default=None, alias="Milestone", description="Milestone")
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
class AssignableEntity(GeneralEntity):
|
|
214
|
+
"""TP's ``Assignable`` base - the work-item shape.
|
|
215
|
+
|
|
216
|
+
``UserStory``, ``Bug``, ``Task``, ``Feature``, ``Epic`` and ``Request``
|
|
217
|
+
derive from this. It adds the effort/time surface, the planning
|
|
218
|
+
references, and the workflow state that make a record schedulable.
|
|
219
|
+
|
|
220
|
+
Two fields read as a priority and are not interchangeable: ``priority`` is
|
|
221
|
+
the entity-type-scoped ``Priority`` record (arriving with an
|
|
222
|
+
``Importance``), while ``numeric_priority`` on :class:`GeneralEntity` is
|
|
223
|
+
TP's continuous backlog-ordering rank.
|
|
224
|
+
|
|
225
|
+
``team`` is deprecated upstream - TP's ``/meta`` marks it so, and
|
|
226
|
+
``responsible_team`` (a ``TeamAssignment`` reference) is the current
|
|
227
|
+
surface - but it is still populated and still declared here.
|
|
228
|
+
|
|
229
|
+
The numeric fields carry no range constraint. TP computes most of them and
|
|
230
|
+
documents no bounds, and a constraint on a server-supplied value fails the
|
|
231
|
+
**whole entity**, not the field: one out-of-range roll-up would abort a
|
|
232
|
+
whole ``list()`` page with a ``ParseError``. A read model that faithfully
|
|
233
|
+
reports what TP sent is worth more than one that asserts an invariant the
|
|
234
|
+
API never promised. Write-side range checks belong where the value
|
|
235
|
+
originates - see ``times.upsert``, which validates before sending.
|
|
236
|
+
|
|
237
|
+
Attributes:
|
|
238
|
+
effort: Total effort estimate
|
|
239
|
+
effort_completed: Completed effort
|
|
240
|
+
effort_todo: Remaining effort
|
|
241
|
+
progress: Completion ratio TP derives from effort (0.0 - 1.0)
|
|
242
|
+
time_spent: Hours logged against the item
|
|
243
|
+
time_remain: Hours still expected
|
|
244
|
+
units: The unit effort is expressed in (e.g. "h", "pt")
|
|
245
|
+
lead_time: Days from creation to completion
|
|
246
|
+
cycle_time: Days from work starting to completion
|
|
247
|
+
last_state_change_date: When the entity state last changed
|
|
248
|
+
planned_start_date: Planned start (shadows the assigned iteration)
|
|
249
|
+
planned_end_date: Planned end (shadows the assigned iteration)
|
|
250
|
+
forecast_end_date: TP's projected completion date
|
|
251
|
+
entity_state: Current workflow state reference
|
|
252
|
+
priority: Priority reference, scoped to this entity type
|
|
253
|
+
release: Release reference
|
|
254
|
+
iteration: Iteration reference
|
|
255
|
+
team_iteration: TeamIteration (team sprint) reference
|
|
256
|
+
team: Team reference (deprecated upstream)
|
|
257
|
+
responsible_team: TeamAssignment reference
|
|
258
|
+
assigned_user: Assigned users
|
|
259
|
+
"""
|
|
260
|
+
|
|
261
|
+
# Effort / time tracking
|
|
262
|
+
effort: float | None = Field(default=None, alias="Effort", description="Total effort")
|
|
263
|
+
effort_completed: float | None = Field(
|
|
264
|
+
default=None, alias="EffortCompleted", description="Completed effort"
|
|
265
|
+
)
|
|
266
|
+
effort_todo: float | None = Field(
|
|
267
|
+
default=None, alias="EffortToDo", description="Remaining effort"
|
|
268
|
+
)
|
|
269
|
+
time_spent: float | None = Field(default=None, alias="TimeSpent", description="Time spent")
|
|
270
|
+
time_remain: float | None = Field(
|
|
271
|
+
default=None, alias="TimeRemain", description="Time remaining"
|
|
272
|
+
)
|
|
273
|
+
progress: float | None = Field(default=None, alias="Progress", description="Completion ratio")
|
|
274
|
+
units: str | None = Field(default=None, alias="Units", description="Effort unit")
|
|
275
|
+
|
|
276
|
+
# Flow metrics
|
|
277
|
+
lead_time: float | None = Field(default=None, alias="LeadTime", description="Lead time (days)")
|
|
278
|
+
cycle_time: float | None = Field(
|
|
279
|
+
default=None, alias="CycleTime", description="Cycle time (days)"
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
# Date tracking
|
|
283
|
+
last_state_change_date: TPDateTime | None = Field(
|
|
284
|
+
default=None, alias="LastStateChangeDate", description="Last state change"
|
|
285
|
+
)
|
|
286
|
+
planned_start_date: TPDateTime | None = Field(
|
|
287
|
+
default=None, alias="PlannedStartDate", description="Planned start"
|
|
288
|
+
)
|
|
289
|
+
planned_end_date: TPDateTime | None = Field(
|
|
290
|
+
default=None, alias="PlannedEndDate", description="Planned end"
|
|
291
|
+
)
|
|
292
|
+
forecast_end_date: TPDateTime | None = Field(
|
|
293
|
+
default=None, alias="ForecastEndDate", description="Forecast end"
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
# Relationships
|
|
297
|
+
entity_state: EntityRef | None = Field(
|
|
298
|
+
default=None, alias="EntityState", description="Workflow state"
|
|
299
|
+
)
|
|
300
|
+
priority: RefWithImportance | None = Field(
|
|
301
|
+
default=None, alias="Priority", description="Priority"
|
|
302
|
+
)
|
|
303
|
+
release: EntityRef | None = Field(default=None, alias="Release", description="Release")
|
|
304
|
+
iteration: EntityRef | None = Field(default=None, alias="Iteration", description="Iteration")
|
|
305
|
+
team_iteration: EntityRef | None = Field(
|
|
306
|
+
default=None, alias="TeamIteration", description="Team iteration"
|
|
307
|
+
)
|
|
308
|
+
team: EntityRef | None = Field(
|
|
309
|
+
default=None, alias="Team", description="Team (deprecated upstream)"
|
|
310
|
+
)
|
|
311
|
+
responsible_team: EntityRef | None = Field(
|
|
312
|
+
default=None, alias="ResponsibleTeam", description="Responsible team assignment"
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
# Collections
|
|
316
|
+
assigned_user: AssignedUsers | None = Field(
|
|
317
|
+
default=None, alias="AssignedUser", description="Assigned users"
|
|
318
|
+
)
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
"""The types that carry content attached to another entity.
|
|
2
|
+
|
|
3
|
+
A ``Comment`` and an ``Attachment`` both hang off a ``General``; a
|
|
4
|
+
``CustomField`` describes a field configured on a process rather than a value
|
|
5
|
+
held by an entity. None sits in TP's ``General`` hierarchy.
|
|
6
|
+
|
|
7
|
+
Re-exported by :mod:`targetprocess.models`, which stays the import surface
|
|
8
|
+
callers use.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
12
|
+
|
|
13
|
+
from targetprocess._base import Entity, NamedEntity
|
|
14
|
+
from targetprocess._dates import TPDateTime
|
|
15
|
+
from targetprocess._nested import CustomFieldConfig, EntityRef, EntityTypeRef, UserRef
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Comment(Entity):
|
|
19
|
+
"""Comment on a TP entity.
|
|
20
|
+
|
|
21
|
+
TP comments carry the comment body in ``Description`` and have no Name
|
|
22
|
+
field. ``General`` is the entity the comment is attached to; ``Owner`` is
|
|
23
|
+
the commenter. Threaded replies carry a ``ParentId``.
|
|
24
|
+
|
|
25
|
+
Attributes:
|
|
26
|
+
description: Comment body text
|
|
27
|
+
parent_id: Parent comment id for threaded replies (None at top level)
|
|
28
|
+
description_modify_date: When the comment body was last edited
|
|
29
|
+
is_private: Whether the comment is private
|
|
30
|
+
is_pinned: Whether the comment is pinned
|
|
31
|
+
general: The entity the comment is attached to
|
|
32
|
+
owner: The commenter
|
|
33
|
+
entity_version: TP's per-record version counter (increments on edit)
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
description: str | None = Field(
|
|
37
|
+
default=None, alias="Description", description="Comment body text"
|
|
38
|
+
)
|
|
39
|
+
parent_id: int | None = Field(
|
|
40
|
+
default=None, alias="ParentId", description="Parent comment id (threaded replies)"
|
|
41
|
+
)
|
|
42
|
+
description_modify_date: TPDateTime | None = Field(
|
|
43
|
+
default=None, alias="DescriptionModifyDate", description="When the body was last edited"
|
|
44
|
+
)
|
|
45
|
+
is_private: bool | None = Field(
|
|
46
|
+
default=None, alias="IsPrivate", description="Whether the comment is private"
|
|
47
|
+
)
|
|
48
|
+
is_pinned: bool | None = Field(
|
|
49
|
+
default=None, alias="IsPinned", description="Whether the comment is pinned"
|
|
50
|
+
)
|
|
51
|
+
general: EntityRef | None = Field(
|
|
52
|
+
default=None, alias="General", description="Attached-to entity"
|
|
53
|
+
)
|
|
54
|
+
owner: UserRef | None = Field(default=None, alias="Owner", description="Commenter")
|
|
55
|
+
entity_version: int | None = Field(
|
|
56
|
+
default=None, alias="EntityVersion", description="Per-record version counter"
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class Attachment(NamedEntity):
|
|
61
|
+
"""File attachment on a TP entity.
|
|
62
|
+
|
|
63
|
+
``Name`` is the display filename; ``UniqueFileName`` is TP's stored name.
|
|
64
|
+
``General`` is the entity the file is attached to; ``Owner`` is the
|
|
65
|
+
uploader. ``Uri`` is the download path for the file's bytes (relative to
|
|
66
|
+
the instance root, e.g. ``/Attachment.aspx?AttachmentID=1234``) - fetch it
|
|
67
|
+
via ``client.attachments.download``.
|
|
68
|
+
|
|
69
|
+
Attributes:
|
|
70
|
+
unique_file_name: TP's stored unique filename
|
|
71
|
+
description: Attachment description
|
|
72
|
+
date: Upload date/time
|
|
73
|
+
owner: The uploader
|
|
74
|
+
general: The entity the file is attached to
|
|
75
|
+
message: Optional attachment message
|
|
76
|
+
uri: Download path for the file bytes, relative to the instance root
|
|
77
|
+
mime_type: MIME type of the stored file
|
|
78
|
+
size: File size in bytes
|
|
79
|
+
thumbnail_uri: Download path for the thumbnail (images only)
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
unique_file_name: str | None = Field(
|
|
83
|
+
default=None, alias="UniqueFileName", description="Stored unique filename"
|
|
84
|
+
)
|
|
85
|
+
description: str | None = Field(
|
|
86
|
+
default=None, alias="Description", description="Attachment description"
|
|
87
|
+
)
|
|
88
|
+
date: TPDateTime | None = Field(default=None, alias="Date", description="Upload date/time")
|
|
89
|
+
owner: UserRef | None = Field(default=None, alias="Owner", description="Uploader")
|
|
90
|
+
general: EntityRef | None = Field(
|
|
91
|
+
default=None, alias="General", description="Attached-to entity"
|
|
92
|
+
)
|
|
93
|
+
message: EntityRef | None = Field(
|
|
94
|
+
default=None, alias="Message", description="Attachment message"
|
|
95
|
+
)
|
|
96
|
+
uri: str | None = Field(
|
|
97
|
+
default=None, alias="Uri", description="Download path for the file bytes"
|
|
98
|
+
)
|
|
99
|
+
mime_type: str | None = Field(default=None, alias="MimeType", description="MIME type")
|
|
100
|
+
size: int | None = Field(default=None, alias="Size", ge=0, description="File size in bytes")
|
|
101
|
+
thumbnail_uri: str | None = Field(
|
|
102
|
+
default=None, alias="ThumbnailUri", description="Thumbnail download path"
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class UploadedFileRef(BaseModel):
|
|
107
|
+
"""A reference nested in an ``/UploadFile.ashx`` response.
|
|
108
|
+
|
|
109
|
+
The same three roles :class:`Attachment` names - the uploader, the entity
|
|
110
|
+
the file landed on, and that entity as an Assignable - but projected by
|
|
111
|
+
the file endpoint rather than the JSON entity API, so the keys are
|
|
112
|
+
camelCase and the user shape carries its name fields inline. One
|
|
113
|
+
permissive model covers all of them: a user reference brings the name
|
|
114
|
+
fields, an entity reference brings ``name``, and ``extra="allow"`` keeps
|
|
115
|
+
anything else the endpoint sends.
|
|
116
|
+
|
|
117
|
+
Not in ``_nested`` with the other reference shapes on purpose: those
|
|
118
|
+
project TP's JSON entity API, and this one does not (see
|
|
119
|
+
:class:`UploadedAttachment`).
|
|
120
|
+
|
|
121
|
+
Attributes:
|
|
122
|
+
resource_type: Type of the referenced record
|
|
123
|
+
id: Id of the referenced record
|
|
124
|
+
name: Display name, on an entity reference
|
|
125
|
+
first_name: Given name, on a user reference
|
|
126
|
+
last_name: Family name, on a user reference
|
|
127
|
+
full_name: Display name, on a user reference
|
|
128
|
+
login: Login, on a user reference
|
|
129
|
+
"""
|
|
130
|
+
|
|
131
|
+
model_config = ConfigDict(
|
|
132
|
+
populate_by_name=True,
|
|
133
|
+
str_strip_whitespace=True,
|
|
134
|
+
validate_assignment=True,
|
|
135
|
+
extra="allow",
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
resource_type: str | None = Field(
|
|
139
|
+
default=None, alias="resourceType", description="Referenced record's type"
|
|
140
|
+
)
|
|
141
|
+
id: int | None = Field(default=None, alias="id", description="Referenced record's Id")
|
|
142
|
+
name: str | None = Field(default=None, alias="name", description="Display name")
|
|
143
|
+
first_name: str | None = Field(default=None, alias="firstName", description="Given name")
|
|
144
|
+
last_name: str | None = Field(default=None, alias="lastName", description="Family name")
|
|
145
|
+
full_name: str | None = Field(default=None, alias="fullName", description="Display name")
|
|
146
|
+
login: str | None = Field(default=None, alias="login", description="Login")
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
class UploadedAttachment(BaseModel):
|
|
150
|
+
"""The attachment record ``/UploadFile.ashx`` answers an upload with.
|
|
151
|
+
|
|
152
|
+
TargetProcess documents neither this endpoint's response body nor its
|
|
153
|
+
casing. What it actually sends - recorded in the write-path integration
|
|
154
|
+
suite rather than taken on trust - is
|
|
155
|
+
``{"items": [<this shape>]}``: the created Attachment, hydrated much as
|
|
156
|
+
``/api/v1/Attachments`` would hydrate it, but in **camelCase** and with
|
|
157
|
+
two extra fields the entity API does not carry (``persistedMimeType``,
|
|
158
|
+
``persistedSize``) alongside their entity-API equivalents. ``date`` is
|
|
159
|
+
ISO-8601 rather than TP's ``/Date(ms±HHMM)/`` wire format, which
|
|
160
|
+
:data:`TPDateTime` passes through to pydantic unchanged.
|
|
161
|
+
|
|
162
|
+
Deliberately outside ``scripts/check_model_coverage.py``'s ``COLLECTIONS``
|
|
163
|
+
map: this is one endpoint's response projection, not a queryable entity
|
|
164
|
+
type, so there is no ``/meta`` to diff it against. The recorded cassette
|
|
165
|
+
is what pins the shape.
|
|
166
|
+
|
|
167
|
+
Attributes:
|
|
168
|
+
resource_type: Always ``"Attachment"``
|
|
169
|
+
id: Id of the created attachment record
|
|
170
|
+
name: Display filename, as sent
|
|
171
|
+
unique_file_name: TP's stored unique filename
|
|
172
|
+
description: Attachment description (the filename, by default)
|
|
173
|
+
date: Upload timestamp
|
|
174
|
+
persisted_mime_type: MIME type of the stored file
|
|
175
|
+
persisted_size: Stored size in bytes
|
|
176
|
+
is_empty: Whether the stored file has no content
|
|
177
|
+
uri: Absolute download URL for the file bytes
|
|
178
|
+
thumbnail_uri: Absolute download URL for the thumbnail
|
|
179
|
+
mime_type: MIME type of the stored file
|
|
180
|
+
size: File size in bytes
|
|
181
|
+
owner: The uploader
|
|
182
|
+
general: The entity the file was attached to
|
|
183
|
+
message: The message the file was attached to, if any
|
|
184
|
+
assignable: The attached-to entity as an Assignable, if it is one
|
|
185
|
+
"""
|
|
186
|
+
|
|
187
|
+
model_config = ConfigDict(
|
|
188
|
+
populate_by_name=True,
|
|
189
|
+
str_strip_whitespace=True,
|
|
190
|
+
validate_assignment=True,
|
|
191
|
+
extra="allow",
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
resource_type: str | None = Field(default=None, alias="resourceType", description="Record type")
|
|
195
|
+
id: int = Field(alias="id", description="Created attachment's Id")
|
|
196
|
+
name: str | None = Field(default=None, alias="name", description="Display filename")
|
|
197
|
+
unique_file_name: str | None = Field(
|
|
198
|
+
default=None, alias="uniqueFileName", description="Stored unique filename"
|
|
199
|
+
)
|
|
200
|
+
description: str | None = Field(
|
|
201
|
+
default=None, alias="description", description="Attachment description"
|
|
202
|
+
)
|
|
203
|
+
date: TPDateTime | None = Field(default=None, alias="date", description="Upload timestamp")
|
|
204
|
+
persisted_mime_type: str | None = Field(
|
|
205
|
+
default=None, alias="persistedMimeType", description="Stored MIME type"
|
|
206
|
+
)
|
|
207
|
+
persisted_size: int | None = Field(
|
|
208
|
+
default=None, alias="persistedSize", ge=0, description="Stored size in bytes"
|
|
209
|
+
)
|
|
210
|
+
is_empty: bool | None = Field(
|
|
211
|
+
default=None, alias="isEmpty", description="Whether the stored file is empty"
|
|
212
|
+
)
|
|
213
|
+
uri: str | None = Field(default=None, alias="uri", description="Absolute download URL")
|
|
214
|
+
thumbnail_uri: str | None = Field(
|
|
215
|
+
default=None, alias="thumbnailUri", description="Absolute thumbnail URL"
|
|
216
|
+
)
|
|
217
|
+
mime_type: str | None = Field(default=None, alias="mimeType", description="MIME type")
|
|
218
|
+
size: int | None = Field(default=None, alias="size", ge=0, description="File size in bytes")
|
|
219
|
+
owner: UploadedFileRef | None = Field(default=None, alias="owner", description="Uploader")
|
|
220
|
+
general: UploadedFileRef | None = Field(
|
|
221
|
+
default=None, alias="general", description="Attached-to entity"
|
|
222
|
+
)
|
|
223
|
+
message: UploadedFileRef | None = Field(
|
|
224
|
+
default=None, alias="message", description="Attached-to message"
|
|
225
|
+
)
|
|
226
|
+
assignable: UploadedFileRef | None = Field(
|
|
227
|
+
default=None, alias="assignable", description="Attached-to entity as an Assignable"
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
class CustomField(NamedEntity):
|
|
232
|
+
"""Custom-field definition (the queryable ``/CustomFields`` entity).
|
|
233
|
+
|
|
234
|
+
Describes a custom field configured on a Process/EntityType - its type,
|
|
235
|
+
constraints, and metadata. Actual per-entity values are carried by
|
|
236
|
+
``CustomFieldValue`` (embedded in each entity's ``CustomFields`` array),
|
|
237
|
+
not here.
|
|
238
|
+
|
|
239
|
+
Attributes:
|
|
240
|
+
value: Field value from the definition endpoint (typically a string or
|
|
241
|
+
None; typed ``object`` to carry any value the endpoint returns)
|
|
242
|
+
field_type: Field type (e.g. "DropDown", "Text", "Date", "Number")
|
|
243
|
+
enabled_for_filter: Whether the field is enabled for filtering
|
|
244
|
+
required: Whether the field is required
|
|
245
|
+
numeric_priority: Ordering priority
|
|
246
|
+
is_system: Whether this is a system field
|
|
247
|
+
description: Field description
|
|
248
|
+
placeholder: Field placeholder text
|
|
249
|
+
max_text_length: Maximum text length
|
|
250
|
+
entity_field_name: Underlying entity field name, where the custom field
|
|
251
|
+
shadows a built-in one
|
|
252
|
+
config: Type-specific configuration (default value, units, formatting)
|
|
253
|
+
entity_type: EntityType the field applies to
|
|
254
|
+
process: Process the field belongs to
|
|
255
|
+
"""
|
|
256
|
+
|
|
257
|
+
value: object = Field(default=None, alias="Value", description="Field value (polymorphic)")
|
|
258
|
+
field_type: str | None = Field(default=None, alias="FieldType", description="Field type")
|
|
259
|
+
enabled_for_filter: bool | None = Field(
|
|
260
|
+
default=None, alias="EnabledForFilter", description="Enabled for filtering"
|
|
261
|
+
)
|
|
262
|
+
required: bool | None = Field(default=None, alias="Required", description="Whether required")
|
|
263
|
+
numeric_priority: float | None = Field(
|
|
264
|
+
default=None, alias="NumericPriority", description="Ordering priority"
|
|
265
|
+
)
|
|
266
|
+
is_system: bool | None = Field(
|
|
267
|
+
default=None, alias="IsSystem", description="Whether a system field"
|
|
268
|
+
)
|
|
269
|
+
description: str | None = Field(
|
|
270
|
+
default=None, alias="Description", description="Field description"
|
|
271
|
+
)
|
|
272
|
+
placeholder: str | None = Field(
|
|
273
|
+
default=None, alias="Placeholder", description="Placeholder text"
|
|
274
|
+
)
|
|
275
|
+
max_text_length: int | None = Field(
|
|
276
|
+
default=None, alias="MaxTextLength", description="Maximum text length"
|
|
277
|
+
)
|
|
278
|
+
entity_field_name: str | None = Field(
|
|
279
|
+
default=None, alias="EntityFieldName", description="Shadowed entity field name"
|
|
280
|
+
)
|
|
281
|
+
config: CustomFieldConfig | None = Field(
|
|
282
|
+
default=None, alias="Config", description="Type-specific configuration"
|
|
283
|
+
)
|
|
284
|
+
entity_type: EntityTypeRef | None = Field(
|
|
285
|
+
default=None, alias="EntityType", description="Applicable entity type"
|
|
286
|
+
)
|
|
287
|
+
process: EntityRef | None = Field(default=None, alias="Process", description="Owning process")
|
targetprocess/_dates.py
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""TP ``/Date(ms±HHMM)/`` wire-format date handling.
|
|
2
|
+
|
|
3
|
+
The public names here (``TPDateTime``, ``parse_tp_date``, ``format_tp_date``)
|
|
4
|
+
are re-exported by :mod:`targetprocess.models`, which remains the import
|
|
5
|
+
surface callers use.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import re
|
|
9
|
+
from datetime import UTC, datetime, timedelta, timezone
|
|
10
|
+
from typing import Annotated
|
|
11
|
+
|
|
12
|
+
from pydantic import BeforeValidator
|
|
13
|
+
|
|
14
|
+
_TP_DATE_RE = re.compile(r"^/Date\((-?\d+)(?:([+-])(\d{2})(\d{2}))?\)/$")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def parse_tp_date(value: object) -> object:
|
|
18
|
+
"""Convert TP's ``/Date(ms±HHMM)/`` wire format to datetime.
|
|
19
|
+
|
|
20
|
+
Non-matching values (datetime, ISO strings, None) pass through for
|
|
21
|
+
pydantic's own datetime handling.
|
|
22
|
+
"""
|
|
23
|
+
if isinstance(value, str):
|
|
24
|
+
m = _TP_DATE_RE.match(value)
|
|
25
|
+
if m:
|
|
26
|
+
ms = int(m.group(1))
|
|
27
|
+
if m.group(2):
|
|
28
|
+
sign = 1 if m.group(2) == "+" else -1
|
|
29
|
+
tz = timezone(sign * timedelta(hours=int(m.group(3)), minutes=int(m.group(4))))
|
|
30
|
+
else:
|
|
31
|
+
tz = UTC
|
|
32
|
+
return datetime.fromtimestamp(ms / 1000, tz=tz)
|
|
33
|
+
return value
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
TPDateTime = Annotated[datetime, BeforeValidator(parse_tp_date)]
|
|
37
|
+
|
|
38
|
+
_EPOCH = datetime(1970, 1, 1, tzinfo=UTC)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def format_tp_date(value: datetime) -> str:
|
|
42
|
+
"""Encode a timezone-aware datetime as TP's ``/Date(ms±HHMM)/`` wire format.
|
|
43
|
+
|
|
44
|
+
The inverse of :func:`parse_tp_date`. The offset written is the one the
|
|
45
|
+
datetime carries, so the caller decides which offset TP records.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
value: Timezone-aware datetime to encode.
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
The TP wire representation, e.g. ``/Date(1718366400000+0200)/``.
|
|
52
|
+
|
|
53
|
+
Raises:
|
|
54
|
+
ValueError: ``value`` is naive, so no offset can be written.
|
|
55
|
+
ValueError: ``value``'s offset is not a whole number of minutes, so
|
|
56
|
+
it cannot be represented in TP's ``±HHMM`` wire format.
|
|
57
|
+
"""
|
|
58
|
+
offset = value.utcoffset()
|
|
59
|
+
if offset is None:
|
|
60
|
+
raise ValueError("format_tp_date requires a timezone-aware datetime")
|
|
61
|
+
if offset.total_seconds() % 60 != 0:
|
|
62
|
+
raise ValueError(f"format_tp_date requires a whole-minute UTC offset, got {offset}")
|
|
63
|
+
# Floor-divide two timedeltas rather than scaling ``timestamp()``: that
|
|
64
|
+
# returns a float, and int() truncates toward zero, which is wrong for
|
|
65
|
+
# pre-epoch instants.
|
|
66
|
+
milliseconds = (value - _EPOCH) // timedelta(milliseconds=1)
|
|
67
|
+
total_minutes = int(offset.total_seconds() // 60)
|
|
68
|
+
sign = "+" if total_minutes >= 0 else "-"
|
|
69
|
+
hours, minutes = divmod(abs(total_minutes), 60)
|
|
70
|
+
return f"/Date({milliseconds}{sign}{hours:02d}{minutes:02d})/"
|