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
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""Resource managers for TargetProcess entities."""
|
|
2
|
+
|
|
3
|
+
from targetprocess.resources.assignments import AssignmentsResource
|
|
4
|
+
from targetprocess.resources.attachments import AttachmentsResource
|
|
5
|
+
from targetprocess.resources.base import BaseResource
|
|
6
|
+
from targetprocess.resources.bugs import BugsResource
|
|
7
|
+
from targetprocess.resources.comments import CommentsResource
|
|
8
|
+
from targetprocess.resources.custom_activities import CustomActivitiesResource
|
|
9
|
+
from targetprocess.resources.custom_fields import CustomFieldsResource
|
|
10
|
+
from targetprocess.resources.custom_rules import CustomRulesResource
|
|
11
|
+
from targetprocess.resources.entities import EntitiesResource
|
|
12
|
+
from targetprocess.resources.entity_states import EntityStatesResource
|
|
13
|
+
from targetprocess.resources.entity_types import EntityTypesResource
|
|
14
|
+
from targetprocess.resources.epics import EpicsResource
|
|
15
|
+
from targetprocess.resources.features import FeaturesResource
|
|
16
|
+
from targetprocess.resources.iterations import IterationsResource
|
|
17
|
+
from targetprocess.resources.priorities import PrioritiesResource
|
|
18
|
+
from targetprocess.resources.processes import ProcessesResource
|
|
19
|
+
from targetprocess.resources.projects import ProjectsResource
|
|
20
|
+
from targetprocess.resources.relation_types import RelationTypesResource
|
|
21
|
+
from targetprocess.resources.relations import RelationsResource
|
|
22
|
+
from targetprocess.resources.releases import ReleasesResource
|
|
23
|
+
from targetprocess.resources.requests import RequestsResource
|
|
24
|
+
from targetprocess.resources.role_efforts import RoleEffortsResource
|
|
25
|
+
from targetprocess.resources.roles import RolesResource
|
|
26
|
+
from targetprocess.resources.severities import SeveritiesResource
|
|
27
|
+
from targetprocess.resources.tasks import TasksResource
|
|
28
|
+
from targetprocess.resources.team_assignments import TeamAssignmentsResource
|
|
29
|
+
from targetprocess.resources.team_iterations import TeamIterationsResource
|
|
30
|
+
from targetprocess.resources.teams import TeamsResource
|
|
31
|
+
from targetprocess.resources.terms import TermsResource
|
|
32
|
+
from targetprocess.resources.test_cases import TestCasesResource
|
|
33
|
+
from targetprocess.resources.times import TimesResource
|
|
34
|
+
from targetprocess.resources.user_stories import UserStoriesResource
|
|
35
|
+
from targetprocess.resources.users import UsersResource
|
|
36
|
+
from targetprocess.resources.workflows import WorkflowsResource
|
|
37
|
+
|
|
38
|
+
__all__ = [
|
|
39
|
+
# Base
|
|
40
|
+
"BaseResource",
|
|
41
|
+
# Typed resources
|
|
42
|
+
"AssignmentsResource",
|
|
43
|
+
"AttachmentsResource",
|
|
44
|
+
"BugsResource",
|
|
45
|
+
"CommentsResource",
|
|
46
|
+
"CustomActivitiesResource",
|
|
47
|
+
"CustomFieldsResource",
|
|
48
|
+
"CustomRulesResource",
|
|
49
|
+
"EntityStatesResource",
|
|
50
|
+
"EntityTypesResource",
|
|
51
|
+
"EpicsResource",
|
|
52
|
+
"FeaturesResource",
|
|
53
|
+
"IterationsResource",
|
|
54
|
+
"PrioritiesResource",
|
|
55
|
+
"ProcessesResource",
|
|
56
|
+
"ProjectsResource",
|
|
57
|
+
"RelationTypesResource",
|
|
58
|
+
"RelationsResource",
|
|
59
|
+
"ReleasesResource",
|
|
60
|
+
"RequestsResource",
|
|
61
|
+
"RoleEffortsResource",
|
|
62
|
+
"RolesResource",
|
|
63
|
+
"SeveritiesResource",
|
|
64
|
+
"TasksResource",
|
|
65
|
+
"TeamAssignmentsResource",
|
|
66
|
+
"TeamIterationsResource",
|
|
67
|
+
"TeamsResource",
|
|
68
|
+
"TermsResource",
|
|
69
|
+
"TestCasesResource",
|
|
70
|
+
"TimesResource",
|
|
71
|
+
"UserStoriesResource",
|
|
72
|
+
"UsersResource",
|
|
73
|
+
"WorkflowsResource",
|
|
74
|
+
# Generic resource
|
|
75
|
+
"EntitiesResource",
|
|
76
|
+
]
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Assignment resource manager."""
|
|
2
|
+
|
|
3
|
+
from targetprocess.models import Assignment
|
|
4
|
+
from targetprocess.resources.base import BaseResource
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class AssignmentsResource(BaseResource[Assignment]):
|
|
8
|
+
"""Resource manager for Assignment entities.
|
|
9
|
+
|
|
10
|
+
An Assignment pairs a user (``GeneralUser``) with a ``Role`` on an
|
|
11
|
+
``Assignable`` - the authoritative record of who is assigned to a work
|
|
12
|
+
item and in which capacity. Creating one takes the three references;
|
|
13
|
+
removing one deletes the Assignment record itself.
|
|
14
|
+
|
|
15
|
+
Example:
|
|
16
|
+
client = TargetProcessClient(...)
|
|
17
|
+
async for a in client.assignments.list(
|
|
18
|
+
where="Assignable.Id eq 123",
|
|
19
|
+
include=["GeneralUser", "Role", "Assignable"],
|
|
20
|
+
):
|
|
21
|
+
print(a.general_user, a.role)
|
|
22
|
+
role = await client.roles.resolve("Developer")
|
|
23
|
+
await client.assignments.create(
|
|
24
|
+
Assignable={"Id": 123}, GeneralUser={"Id": 6}, Role={"Id": role.id}
|
|
25
|
+
)
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
entity_type = "Assignment"
|
|
29
|
+
model_class = Assignment
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"""Attachment resource manager."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
|
|
5
|
+
import pydantic
|
|
6
|
+
|
|
7
|
+
from targetprocess.exceptions import ParseError
|
|
8
|
+
from targetprocess.models import Attachment, UploadedAttachment
|
|
9
|
+
from targetprocess.resources.base import BaseResource
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class AttachmentsResource(BaseResource[Attachment]):
|
|
13
|
+
"""Resource manager for Attachment entities.
|
|
14
|
+
|
|
15
|
+
Provides the usual typed CRUD surface over attachment *records*, plus the
|
|
16
|
+
two file-transfer helpers TargetProcess keeps outside its JSON entity API:
|
|
17
|
+
``download`` fetches an attachment's bytes, and ``upload`` sends a new
|
|
18
|
+
file via the vendor-documented multipart endpoint, returning the created
|
|
19
|
+
record as :class:`~targetprocess.models.UploadedAttachment`. ``general``
|
|
20
|
+
names the entity a file is attached to; ``uri`` on a fetched record is
|
|
21
|
+
the instance-root download path ``download`` resolves - and is only
|
|
22
|
+
populated when asked for by ``include=``, a plain ``get`` leaving it,
|
|
23
|
+
``mime_type`` and ``size`` unset.
|
|
24
|
+
|
|
25
|
+
TargetProcess documents its file endpoints against Basic auth, and the
|
|
26
|
+
vendor guide states that a REST API token cannot be used to download an
|
|
27
|
+
attached file. That is not what a token-auth client actually meets: the
|
|
28
|
+
write-path integration suite records both an upload and a download of the
|
|
29
|
+
uploaded bytes succeeding under token auth. The redirect these helpers
|
|
30
|
+
refuse is therefore a real failure to surface, not the expected outcome -
|
|
31
|
+
a 3xx here means the file was not transferred.
|
|
32
|
+
|
|
33
|
+
Example:
|
|
34
|
+
client = TargetProcessClient(...)
|
|
35
|
+
attachment = await client.attachments.get(1234)
|
|
36
|
+
content = await client.attachments.download(1234)
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
entity_type = "Attachment"
|
|
40
|
+
model_class = Attachment
|
|
41
|
+
|
|
42
|
+
async def download(self, id: int) -> bytes:
|
|
43
|
+
"""Download an attachment's file content.
|
|
44
|
+
|
|
45
|
+
Fetches the bytes from the instance-root download path
|
|
46
|
+
(``/Attachment.aspx?AttachmentID=<id>`` - the same path a fetched
|
|
47
|
+
record carries in ``uri``), which sits outside ``/api/v1``.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
id: Attachment ID
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
The file content
|
|
54
|
+
|
|
55
|
+
Raises:
|
|
56
|
+
APIError: The server redirected instead of serving the file
|
|
57
|
+
(typically the login page, when the auth scheme is not
|
|
58
|
+
accepted for downloads - see the class docstring), or
|
|
59
|
+
another API error occurred.
|
|
60
|
+
AuthenticationError: Invalid credentials
|
|
61
|
+
ForbiddenError: Insufficient permissions
|
|
62
|
+
NotFoundError: Attachment not found
|
|
63
|
+
NetworkError: Transport-level failure
|
|
64
|
+
"""
|
|
65
|
+
return await self._request_handler.download_file(f"/Attachment.aspx?AttachmentID={id}")
|
|
66
|
+
|
|
67
|
+
async def upload(
|
|
68
|
+
self,
|
|
69
|
+
general_id: int,
|
|
70
|
+
filename: str,
|
|
71
|
+
content: bytes,
|
|
72
|
+
*,
|
|
73
|
+
mime_type: str | None = None,
|
|
74
|
+
) -> UploadedAttachment:
|
|
75
|
+
"""Upload a file as an attachment on an entity.
|
|
76
|
+
|
|
77
|
+
Requires client mode to be READWRITE. Sends the vendor-documented
|
|
78
|
+
wire shape - ``POST /UploadFile.ashx`` as ``multipart/form-data``
|
|
79
|
+
with a ``generalid`` form field and the file as a ``file`` part.
|
|
80
|
+
|
|
81
|
+
TargetProcess documents the request but not the response. What it
|
|
82
|
+
answers with, recorded in the write-path integration suite, is a
|
|
83
|
+
camelCase ``{"items": [<attachment>]}`` envelope carrying the created
|
|
84
|
+
record - so the created attachment is returned directly and there is
|
|
85
|
+
no need to re-list the entity's attachments to find it. The envelope
|
|
86
|
+
is undocumented and therefore unguaranteed: a body this cannot parse
|
|
87
|
+
raises ``ParseError`` rather than being returned unread, since a
|
|
88
|
+
caller has no way to tell an unparsed body from a failed upload.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
general_id: ID of the entity to attach the file to
|
|
92
|
+
filename: Filename to store the upload under
|
|
93
|
+
content: File content
|
|
94
|
+
mime_type: Optional MIME type for the file part (omitted, the
|
|
95
|
+
server infers one)
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
The created attachment record, as the upload endpoint reports it
|
|
99
|
+
|
|
100
|
+
Raises:
|
|
101
|
+
ReadOnlyViolation: Client is in readonly mode
|
|
102
|
+
ParseError: The response was not the single-item ``items``
|
|
103
|
+
envelope this endpoint is recorded as sending.
|
|
104
|
+
APIError: The server redirected instead of accepting the upload
|
|
105
|
+
(see the class docstring), or another API error occurred.
|
|
106
|
+
AuthenticationError: Invalid credentials
|
|
107
|
+
ForbiddenError: Insufficient permissions
|
|
108
|
+
NetworkError: Transport-level failure
|
|
109
|
+
"""
|
|
110
|
+
self._client._check_write_permission()
|
|
111
|
+
file_part = (filename, content, mime_type) if mime_type else (filename, content)
|
|
112
|
+
body = await self._request_handler.upload_file(
|
|
113
|
+
"/UploadFile.ashx",
|
|
114
|
+
data={"generalid": str(general_id)},
|
|
115
|
+
files={"file": file_part},
|
|
116
|
+
)
|
|
117
|
+
return self._parse_upload_response(body)
|
|
118
|
+
|
|
119
|
+
@staticmethod
|
|
120
|
+
def _parse_upload_response(body: str) -> UploadedAttachment:
|
|
121
|
+
"""Parse the ``/UploadFile.ashx`` envelope into the created record.
|
|
122
|
+
|
|
123
|
+
Separated from ``upload`` so the envelope's shape can be exercised
|
|
124
|
+
without a transport: the endpoint is undocumented, so every failure
|
|
125
|
+
mode here is a guess about a real server's behaviour and wants a test
|
|
126
|
+
of its own.
|
|
127
|
+
|
|
128
|
+
Raises:
|
|
129
|
+
ParseError: The body was not JSON, or not an ``items`` envelope
|
|
130
|
+
holding exactly the one record a single-file upload creates.
|
|
131
|
+
"""
|
|
132
|
+
try:
|
|
133
|
+
payload = json.loads(body)
|
|
134
|
+
except (json.JSONDecodeError, TypeError) as exc:
|
|
135
|
+
raise ParseError(
|
|
136
|
+
"upload response was not JSON; an HTML login page is one way this "
|
|
137
|
+
"happens, TargetProcess documenting its file endpoints against "
|
|
138
|
+
"Basic auth"
|
|
139
|
+
) from exc
|
|
140
|
+
items = payload.get("items") if isinstance(payload, dict) else None
|
|
141
|
+
if not isinstance(items, list) or len(items) != 1 or not isinstance(items[0], dict):
|
|
142
|
+
raise ParseError(
|
|
143
|
+
"upload response was not an 'items' envelope holding exactly one "
|
|
144
|
+
f"record (got {type(payload).__name__} with keys "
|
|
145
|
+
f"{sorted(payload) if isinstance(payload, dict) else 'n/a'})"
|
|
146
|
+
)
|
|
147
|
+
# Every field but ``id`` is optional, so without this a bare
|
|
148
|
+
# ``{"id": 1}`` - or a record of some other type entirely - would
|
|
149
|
+
# hydrate cleanly and be returned as a created attachment. The
|
|
150
|
+
# endpoint is undocumented, which is the reason to check what it
|
|
151
|
+
# claims to have made rather than the reason to trust it.
|
|
152
|
+
resource_type = items[0].get("resourceType")
|
|
153
|
+
if resource_type != "Attachment":
|
|
154
|
+
raise ParseError(
|
|
155
|
+
"upload response did not describe an Attachment "
|
|
156
|
+
f"(resourceType was {resource_type!r})"
|
|
157
|
+
)
|
|
158
|
+
try:
|
|
159
|
+
return UploadedAttachment.model_validate(items[0])
|
|
160
|
+
except pydantic.ValidationError as exc:
|
|
161
|
+
# Every other parse in the library surfaces a model-validation
|
|
162
|
+
# failure as ParseError, and a caller guarding an upload with
|
|
163
|
+
# ``except TargetProcessError`` would not catch pydantic's own.
|
|
164
|
+
raise ParseError(f"failed to parse UploadedAttachment: {exc}") from exc
|