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.
Files changed (58) hide show
  1. targetprocess/__init__.py +231 -0
  2. targetprocess/_assignables.py +162 -0
  3. targetprocess/_base.py +318 -0
  4. targetprocess/_content.py +287 -0
  5. targetprocess/_dates.py +70 -0
  6. targetprocess/_entity_types.py +33 -0
  7. targetprocess/_generals.py +324 -0
  8. targetprocess/_joins.py +217 -0
  9. targetprocess/_lookups.py +477 -0
  10. targetprocess/_nested.py +223 -0
  11. targetprocess/_observability.py +368 -0
  12. targetprocess/client.py +430 -0
  13. targetprocess/exceptions.py +147 -0
  14. targetprocess/models.py +121 -0
  15. targetprocess/py.typed +1 -0
  16. targetprocess/request_handler.py +724 -0
  17. targetprocess/resources/__init__.py +76 -0
  18. targetprocess/resources/assignments.py +29 -0
  19. targetprocess/resources/attachments.py +164 -0
  20. targetprocess/resources/base.py +563 -0
  21. targetprocess/resources/bugs.py +20 -0
  22. targetprocess/resources/comments.py +23 -0
  23. targetprocess/resources/custom_activities.py +56 -0
  24. targetprocess/resources/custom_fields.py +27 -0
  25. targetprocess/resources/custom_rules.py +28 -0
  26. targetprocess/resources/entities.py +433 -0
  27. targetprocess/resources/entity_states.py +20 -0
  28. targetprocess/resources/entity_types.py +61 -0
  29. targetprocess/resources/epics.py +20 -0
  30. targetprocess/resources/features.py +20 -0
  31. targetprocess/resources/iterations.py +20 -0
  32. targetprocess/resources/priorities.py +85 -0
  33. targetprocess/resources/processes.py +65 -0
  34. targetprocess/resources/projects.py +20 -0
  35. targetprocess/resources/relation_types.py +59 -0
  36. targetprocess/resources/relations.py +34 -0
  37. targetprocess/resources/releases.py +20 -0
  38. targetprocess/resources/requests.py +20 -0
  39. targetprocess/resources/role_efforts.py +21 -0
  40. targetprocess/resources/roles.py +52 -0
  41. targetprocess/resources/severities.py +53 -0
  42. targetprocess/resources/tasks.py +20 -0
  43. targetprocess/resources/team_assignments.py +21 -0
  44. targetprocess/resources/team_iterations.py +24 -0
  45. targetprocess/resources/teams.py +20 -0
  46. targetprocess/resources/terms.py +28 -0
  47. targetprocess/resources/test_cases.py +20 -0
  48. targetprocess/resources/times.py +392 -0
  49. targetprocess/resources/user_stories.py +20 -0
  50. targetprocess/resources/users.py +20 -0
  51. targetprocess/resources/workflows.py +27 -0
  52. targetprocess/response_parser.py +113 -0
  53. targetprocess/transport.py +116 -0
  54. targetprocess/types.py +56 -0
  55. targetprocess_py-0.1.0.dist-info/METADATA +476 -0
  56. targetprocess_py-0.1.0.dist-info/RECORD +58 -0
  57. targetprocess_py-0.1.0.dist-info/WHEEL +4 -0
  58. 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