trackerkit 0.3.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 (44) hide show
  1. trackerkit/__init__.py +81 -0
  2. trackerkit/contracts/__init__.py +35 -0
  3. trackerkit/contracts/auth.py +91 -0
  4. trackerkit/contracts/comments.py +14 -0
  5. trackerkit/contracts/connection.py +14 -0
  6. trackerkit/contracts/projects.py +43 -0
  7. trackerkit/contracts/relations.py +30 -0
  8. trackerkit/contracts/task_tracker_client.py +25 -0
  9. trackerkit/contracts/tasks.py +28 -0
  10. trackerkit/contracts/users.py +10 -0
  11. trackerkit/domain/__init__.py +70 -0
  12. trackerkit/domain/enums.py +28 -0
  13. trackerkit/domain/errors.py +37 -0
  14. trackerkit/domain/models.py +131 -0
  15. trackerkit/domain/relation_mapping.py +215 -0
  16. trackerkit/factory/__init__.py +4 -0
  17. trackerkit/factory/client_factory.py +105 -0
  18. trackerkit/providers/__init__.py +6 -0
  19. trackerkit/providers/asana/__init__.py +4 -0
  20. trackerkit/providers/asana/client.py +189 -0
  21. trackerkit/providers/asana/errors.py +12 -0
  22. trackerkit/providers/asana/mappers.py +84 -0
  23. trackerkit/providers/asana/queries.py +58 -0
  24. trackerkit/providers/asana/transport.py +193 -0
  25. trackerkit/providers/base.py +47 -0
  26. trackerkit/providers/jira/__init__.py +4 -0
  27. trackerkit/providers/jira/client.py +203 -0
  28. trackerkit/providers/jira/errors.py +29 -0
  29. trackerkit/providers/jira/mappers.py +126 -0
  30. trackerkit/providers/jira/queries.py +39 -0
  31. trackerkit/providers/jira/relations.py +198 -0
  32. trackerkit/providers/jira/transport.py +176 -0
  33. trackerkit/providers/yandex_tracker/__init__.py +4 -0
  34. trackerkit/providers/yandex_tracker/client.py +283 -0
  35. trackerkit/providers/yandex_tracker/errors.py +17 -0
  36. trackerkit/providers/yandex_tracker/mappers.py +95 -0
  37. trackerkit/providers/yandex_tracker/queries.py +62 -0
  38. trackerkit/providers/yandex_tracker/relations.py +165 -0
  39. trackerkit/providers/yandex_tracker/transport.py +127 -0
  40. trackerkit/tracker_client.py +232 -0
  41. trackerkit-0.3.0.dist-info/METADATA +300 -0
  42. trackerkit-0.3.0.dist-info/RECORD +44 -0
  43. trackerkit-0.3.0.dist-info/WHEEL +4 -0
  44. trackerkit-0.3.0.dist-info/licenses/LICENSE +21 -0
trackerkit/__init__.py ADDED
@@ -0,0 +1,81 @@
1
+ """Public package entrypoint for trackerkit."""
2
+
3
+ from .contracts.auth import (
4
+ AsanaAuthConfig,
5
+ JiraAuthConfig,
6
+ ProviderAuthConfig,
7
+ YandexTrackerAuthConfig,
8
+ )
9
+ from .domain import (
10
+ AuthenticationError,
11
+ Comment,
12
+ ConfigurationError,
13
+ ConnectionDiagnostic,
14
+ ConnectionErrorKind,
15
+ CreateCommentInput,
16
+ CreateProjectInput,
17
+ CreateRelationInput,
18
+ CreateTaskInput,
19
+ JiraContainsMode,
20
+ JiraLinkTypeMapping,
21
+ JiraRelationMappingConfig,
22
+ Project,
23
+ Provider,
24
+ ProviderCapabilityError,
25
+ ProviderError,
26
+ Relation,
27
+ RelationMappingConfig,
28
+ RelationType,
29
+ Status,
30
+ Task,
31
+ TaskQuery,
32
+ TrackerClientError,
33
+ TrackerKitError,
34
+ UpdateProjectInput,
35
+ UpdateRelationInput,
36
+ UpdateTaskInput,
37
+ User,
38
+ Workspace,
39
+ YandexTrackerRelationMapping,
40
+ YandexTrackerRelationMappingConfig,
41
+ )
42
+ from .tracker_client import TrackerClient
43
+
44
+ __all__ = [
45
+ "AsanaAuthConfig",
46
+ "AuthenticationError",
47
+ "Comment",
48
+ "ConfigurationError",
49
+ "ConnectionDiagnostic",
50
+ "ConnectionErrorKind",
51
+ "CreateCommentInput",
52
+ "CreateProjectInput",
53
+ "CreateRelationInput",
54
+ "CreateTaskInput",
55
+ "JiraAuthConfig",
56
+ "JiraContainsMode",
57
+ "JiraLinkTypeMapping",
58
+ "JiraRelationMappingConfig",
59
+ "Project",
60
+ "Provider",
61
+ "ProviderAuthConfig",
62
+ "ProviderCapabilityError",
63
+ "ProviderError",
64
+ "Relation",
65
+ "RelationMappingConfig",
66
+ "RelationType",
67
+ "Status",
68
+ "Task",
69
+ "TaskQuery",
70
+ "TrackerClient",
71
+ "TrackerClientError",
72
+ "TrackerKitError",
73
+ "UpdateProjectInput",
74
+ "UpdateRelationInput",
75
+ "UpdateTaskInput",
76
+ "User",
77
+ "Workspace",
78
+ "YandexTrackerAuthConfig",
79
+ "YandexTrackerRelationMapping",
80
+ "YandexTrackerRelationMappingConfig",
81
+ ]
@@ -0,0 +1,35 @@
1
+ from trackerkit.contracts.auth import (
2
+ AsanaAuthConfig,
3
+ JiraAuthConfig,
4
+ ProviderAuthConfig,
5
+ YandexTrackerAuthConfig,
6
+ )
7
+ from trackerkit.contracts.comments import CommentCapability
8
+ from trackerkit.contracts.connection import ConnectionCapability
9
+ from trackerkit.contracts.projects import (
10
+ ProjectReadCapability,
11
+ ProjectWriteCapability,
12
+ WorkspaceCapability,
13
+ )
14
+ from trackerkit.contracts.relations import RelationCapability
15
+ from trackerkit.contracts.task_tracker_client import TaskTrackerClient
16
+ from trackerkit.contracts.tasks import TaskReadCapability, TaskWriteCapability
17
+ from trackerkit.contracts.users import UserCapability
18
+
19
+ __all__ = [
20
+ "AsanaAuthConfig",
21
+ "CommentCapability",
22
+ "ConnectionCapability",
23
+ "JiraAuthConfig",
24
+ "ProjectReadCapability",
25
+ "ProjectWriteCapability",
26
+ "ProviderAuthConfig",
27
+ "RelationCapability",
28
+ "TaskReadCapability",
29
+ "TaskTrackerClient",
30
+ "TaskWriteCapability",
31
+ "UserCapability",
32
+ "WorkspaceCapability",
33
+ "YandexTrackerAuthConfig",
34
+ ]
35
+
@@ -0,0 +1,91 @@
1
+ from pydantic import BaseModel, ConfigDict, Field, model_validator
2
+
3
+ from trackerkit.domain.enums import Provider
4
+ from trackerkit.domain.errors import ConfigurationError
5
+
6
+
7
+ class ProviderAuthConfig(BaseModel):
8
+ """Base auth config shared by all provider-specific auth models."""
9
+
10
+ # `extra="ignore"` lets the facade pass provider-agnostic kwargs
11
+ # (e.g. `max_retries`) without leaking provider checks into it.
12
+ model_config = ConfigDict(frozen=True, extra="ignore")
13
+
14
+ provider: Provider
15
+ timeout_seconds: float = Field(default=5.0, gt=0)
16
+
17
+
18
+ class JiraAuthConfig(ProviderAuthConfig):
19
+ """Jira auth data.
20
+
21
+ Expected fields:
22
+ - `base_url`
23
+ - exactly one of `access_token` or `api_token`
24
+ - optional `max_retries`
25
+ """
26
+
27
+ provider: Provider = Provider.JIRA
28
+ base_url: str
29
+ email: str | None = None
30
+ api_token: str | None = None
31
+ access_token: str | None = None
32
+ max_retries: int = Field(default=2, ge=0)
33
+
34
+ @model_validator(mode="after")
35
+ def validate_auth(self) -> "JiraAuthConfig":
36
+ if not self.api_token and not self.access_token:
37
+ raise ConfigurationError(
38
+ "JiraAuthConfig requires either api_token or access_token."
39
+ )
40
+ if self.api_token and self.access_token:
41
+ raise ConfigurationError(
42
+ "JiraAuthConfig expects only one of api_token or access_token."
43
+ )
44
+ return self
45
+
46
+
47
+ class YandexTrackerAuthConfig(ProviderAuthConfig):
48
+ """Yandex Tracker auth data.
49
+
50
+ Expected fields:
51
+ - exactly one of `token` or `iam_token`
52
+ - exactly one of `org_id` or `cloud_org_id`
53
+ """
54
+
55
+ provider: Provider = Provider.YANDEX_TRACKER
56
+ token: str | None = None
57
+ iam_token: str | None = None
58
+ org_id: str | None = None
59
+ cloud_org_id: str | None = None
60
+
61
+ @model_validator(mode="after")
62
+ def validate_auth(self) -> "YandexTrackerAuthConfig":
63
+ if not self.token and not self.iam_token:
64
+ raise ConfigurationError(
65
+ "YandexTrackerAuthConfig requires either token or iam_token."
66
+ )
67
+ if self.token and self.iam_token:
68
+ raise ConfigurationError(
69
+ "YandexTrackerAuthConfig expects only one of token or iam_token."
70
+ )
71
+ if not self.org_id and not self.cloud_org_id:
72
+ raise ConfigurationError(
73
+ "YandexTrackerAuthConfig requires either org_id or cloud_org_id."
74
+ )
75
+ if self.org_id and self.cloud_org_id:
76
+ raise ConfigurationError(
77
+ "YandexTrackerAuthConfig expects only one of org_id or cloud_org_id."
78
+ )
79
+ return self
80
+
81
+
82
+ class AsanaAuthConfig(ProviderAuthConfig):
83
+ """Asana auth data.
84
+
85
+ Expected fields:
86
+ - `access_token`
87
+ """
88
+
89
+ provider: Provider = Provider.ASANA
90
+ access_token: str
91
+
@@ -0,0 +1,14 @@
1
+ from abc import ABC, abstractmethod
2
+
3
+ from trackerkit.domain.models import Comment, CreateCommentInput
4
+
5
+
6
+ class CommentCapability(ABC):
7
+ @abstractmethod
8
+ async def list_comments(self, task_id: str) -> list[Comment]:
9
+ raise NotImplementedError
10
+
11
+ @abstractmethod
12
+ async def create_comment(self, payload: CreateCommentInput) -> Comment:
13
+ raise NotImplementedError
14
+
@@ -0,0 +1,14 @@
1
+ from abc import ABC, abstractmethod
2
+
3
+ from trackerkit.domain.models import ConnectionDiagnostic
4
+
5
+
6
+ class ConnectionCapability(ABC):
7
+ @abstractmethod
8
+ async def check_connection(self) -> bool:
9
+ raise NotImplementedError
10
+
11
+ @abstractmethod
12
+ async def get_connection_diagnostic(self) -> ConnectionDiagnostic:
13
+ raise NotImplementedError
14
+
@@ -0,0 +1,43 @@
1
+ from abc import ABC, abstractmethod
2
+
3
+ from trackerkit.domain.models import (
4
+ CreateProjectInput,
5
+ Project,
6
+ UpdateProjectInput,
7
+ Workspace,
8
+ )
9
+
10
+
11
+ class WorkspaceCapability(ABC):
12
+ @abstractmethod
13
+ async def list_workspaces(self) -> list[Workspace]:
14
+ raise NotImplementedError
15
+
16
+
17
+ class ProjectReadCapability(ABC):
18
+ @abstractmethod
19
+ async def get_project(self, project_id: str) -> Project:
20
+ raise NotImplementedError
21
+
22
+ @abstractmethod
23
+ async def list_projects(self, workspace_id: str | None = None) -> list[Project]:
24
+ raise NotImplementedError
25
+
26
+
27
+ class ProjectWriteCapability(ABC):
28
+ @abstractmethod
29
+ async def create_project(self, payload: CreateProjectInput) -> Project:
30
+ raise NotImplementedError
31
+
32
+ @abstractmethod
33
+ async def update_project(
34
+ self,
35
+ project_id: str,
36
+ payload: UpdateProjectInput,
37
+ ) -> Project:
38
+ raise NotImplementedError
39
+
40
+ @abstractmethod
41
+ async def delete_project(self, project_id: str) -> None:
42
+ raise NotImplementedError
43
+
@@ -0,0 +1,30 @@
1
+ from abc import ABC, abstractmethod
2
+
3
+ from trackerkit.domain.models import (
4
+ CreateRelationInput,
5
+ Relation,
6
+ UpdateRelationInput,
7
+ )
8
+
9
+
10
+ class RelationCapability(ABC):
11
+ @abstractmethod
12
+ async def list_relations(self, task_id: str) -> list[Relation]:
13
+ raise NotImplementedError
14
+
15
+ @abstractmethod
16
+ async def create_relation(self, payload: CreateRelationInput) -> Relation:
17
+ raise NotImplementedError
18
+
19
+ @abstractmethod
20
+ async def update_relation(
21
+ self,
22
+ relation_id: str,
23
+ payload: UpdateRelationInput,
24
+ ) -> Relation:
25
+ raise NotImplementedError
26
+
27
+ @abstractmethod
28
+ async def delete_relation(self, relation_id: str) -> None:
29
+ raise NotImplementedError
30
+
@@ -0,0 +1,25 @@
1
+ from trackerkit.contracts.comments import CommentCapability
2
+ from trackerkit.contracts.connection import ConnectionCapability
3
+ from trackerkit.contracts.projects import (
4
+ ProjectReadCapability,
5
+ ProjectWriteCapability,
6
+ WorkspaceCapability,
7
+ )
8
+ from trackerkit.contracts.relations import RelationCapability
9
+ from trackerkit.contracts.tasks import TaskReadCapability, TaskWriteCapability
10
+ from trackerkit.contracts.users import UserCapability
11
+
12
+
13
+ class TaskTrackerClient(
14
+ ConnectionCapability,
15
+ TaskReadCapability,
16
+ TaskWriteCapability,
17
+ WorkspaceCapability,
18
+ ProjectReadCapability,
19
+ ProjectWriteCapability,
20
+ UserCapability,
21
+ CommentCapability,
22
+ RelationCapability,
23
+ ):
24
+ """Unified async port composed from small capability contracts."""
25
+
@@ -0,0 +1,28 @@
1
+ from abc import ABC, abstractmethod
2
+
3
+ from trackerkit.domain.models import CreateTaskInput, Task, TaskQuery, UpdateTaskInput
4
+
5
+
6
+ class TaskReadCapability(ABC):
7
+ @abstractmethod
8
+ async def get_task(self, task_id: str) -> Task:
9
+ raise NotImplementedError
10
+
11
+ @abstractmethod
12
+ async def list_tasks(self, query: TaskQuery | None = None) -> list[Task]:
13
+ raise NotImplementedError
14
+
15
+
16
+ class TaskWriteCapability(ABC):
17
+ @abstractmethod
18
+ async def create_task(self, payload: CreateTaskInput) -> Task:
19
+ raise NotImplementedError
20
+
21
+ @abstractmethod
22
+ async def update_task(self, task_id: str, payload: UpdateTaskInput) -> Task:
23
+ raise NotImplementedError
24
+
25
+ @abstractmethod
26
+ async def delete_task(self, task_id: str) -> None:
27
+ raise NotImplementedError
28
+
@@ -0,0 +1,10 @@
1
+ from abc import ABC, abstractmethod
2
+
3
+ from trackerkit.domain.models import User
4
+
5
+
6
+ class UserCapability(ABC):
7
+ @abstractmethod
8
+ async def list_users(self) -> list[User]:
9
+ raise NotImplementedError
10
+
@@ -0,0 +1,70 @@
1
+ from trackerkit.domain.enums import ConnectionErrorKind, Provider, RelationType
2
+ from trackerkit.domain.errors import (
3
+ AuthenticationError,
4
+ ConfigurationError,
5
+ ProviderCapabilityError,
6
+ ProviderError,
7
+ TrackerClientError,
8
+ TrackerKitError,
9
+ )
10
+ from trackerkit.domain.models import (
11
+ Comment,
12
+ ConnectionDiagnostic,
13
+ CreateCommentInput,
14
+ CreateProjectInput,
15
+ CreateRelationInput,
16
+ CreateTaskInput,
17
+ Project,
18
+ Relation,
19
+ Status,
20
+ Task,
21
+ TaskQuery,
22
+ UpdateProjectInput,
23
+ UpdateRelationInput,
24
+ UpdateTaskInput,
25
+ User,
26
+ Workspace,
27
+ )
28
+ from trackerkit.domain.relation_mapping import (
29
+ JiraContainsMode,
30
+ JiraLinkTypeMapping,
31
+ JiraRelationMappingConfig,
32
+ RelationMappingConfig,
33
+ YandexTrackerRelationMapping,
34
+ YandexTrackerRelationMappingConfig,
35
+ )
36
+
37
+ __all__ = [
38
+ "AuthenticationError",
39
+ "Comment",
40
+ "ConfigurationError",
41
+ "ConnectionDiagnostic",
42
+ "ConnectionErrorKind",
43
+ "CreateCommentInput",
44
+ "CreateProjectInput",
45
+ "CreateRelationInput",
46
+ "CreateTaskInput",
47
+ "JiraContainsMode",
48
+ "JiraLinkTypeMapping",
49
+ "JiraRelationMappingConfig",
50
+ "Project",
51
+ "Provider",
52
+ "ProviderCapabilityError",
53
+ "ProviderError",
54
+ "Relation",
55
+ "RelationMappingConfig",
56
+ "RelationType",
57
+ "Status",
58
+ "Task",
59
+ "TaskQuery",
60
+ "TrackerClientError",
61
+ "TrackerKitError",
62
+ "UpdateProjectInput",
63
+ "UpdateRelationInput",
64
+ "UpdateTaskInput",
65
+ "User",
66
+ "Workspace",
67
+ "YandexTrackerRelationMapping",
68
+ "YandexTrackerRelationMappingConfig",
69
+ ]
70
+
@@ -0,0 +1,28 @@
1
+ from enum import Enum
2
+
3
+
4
+ class Provider(str, Enum):
5
+ JIRA = "jira"
6
+ YANDEX_TRACKER = "yandex_tracker"
7
+ ASANA = "asana"
8
+
9
+
10
+ class RelationType(str, Enum):
11
+ RELATES = "relates"
12
+ BLOCKS = "blocks"
13
+ CONTAINS = "contains"
14
+
15
+
16
+ class ConnectionErrorKind(str, Enum):
17
+ """Stable taxonomy of connection-check failure reasons.
18
+
19
+ `ConnectionDiagnostic.error_kind` uses these values; the string base
20
+ keeps backward compatibility with callers that compare against literals
21
+ like ``"authentication"``.
22
+ """
23
+
24
+ AUTHENTICATION = "authentication"
25
+ CONFIGURATION = "configuration"
26
+ CAPABILITY = "capability"
27
+ PROVIDER = "provider"
28
+
@@ -0,0 +1,37 @@
1
+ from trackerkit.domain.enums import ConnectionErrorKind
2
+
3
+
4
+ class TrackerClientError(Exception):
5
+ """Base exception for the package."""
6
+
7
+
8
+ class ConfigurationError(TrackerClientError):
9
+ """Raised when client configuration is invalid."""
10
+
11
+
12
+ class AuthenticationError(TrackerClientError):
13
+ """Raised when provider authentication fails."""
14
+
15
+
16
+ class ProviderError(TrackerClientError):
17
+ """Raised when a provider-specific operation fails."""
18
+
19
+
20
+ class ProviderCapabilityError(TrackerClientError):
21
+ """Raised when a provider does not support a requested capability."""
22
+
23
+
24
+ TrackerKitError = TrackerClientError
25
+
26
+
27
+ def get_error_kind(error: Exception) -> ConnectionErrorKind:
28
+ if isinstance(error, AuthenticationError):
29
+ return ConnectionErrorKind.AUTHENTICATION
30
+ if isinstance(error, ConfigurationError):
31
+ return ConnectionErrorKind.CONFIGURATION
32
+ if isinstance(error, ProviderCapabilityError):
33
+ return ConnectionErrorKind.CAPABILITY
34
+ if isinstance(error, ProviderError):
35
+ return ConnectionErrorKind.PROVIDER
36
+ return ConnectionErrorKind.PROVIDER
37
+
@@ -0,0 +1,131 @@
1
+ from datetime import datetime
2
+
3
+ from pydantic import BaseModel
4
+
5
+ from trackerkit.domain.enums import ConnectionErrorKind, Provider, RelationType
6
+
7
+
8
+ class User(BaseModel):
9
+ id: str
10
+ display_name: str
11
+ email: str | None = None
12
+
13
+
14
+ class Workspace(BaseModel):
15
+ id: str
16
+ name: str
17
+ key: str | None = None
18
+
19
+
20
+ class ConnectionDiagnostic(BaseModel):
21
+ provider: Provider
22
+ is_connected: bool
23
+ # Pydantic accepts both the enum and its string value (it's a `str` enum),
24
+ # so existing callers passing literals like ``"authentication"`` keep working.
25
+ error_kind: ConnectionErrorKind | None = None
26
+ message: str | None = None
27
+ error_type: str | None = None
28
+
29
+
30
+ class Project(BaseModel):
31
+ id: str
32
+ name: str
33
+ key: str | None = None
34
+ description: str | None = None
35
+ workspace_id: str | None = None
36
+
37
+
38
+ class Status(BaseModel):
39
+ id: str
40
+ name: str
41
+ category: str | None = None
42
+
43
+
44
+ class Task(BaseModel):
45
+ id: str
46
+ key: str | None = None
47
+ title: str
48
+ description: str | None = None
49
+ project_id: str | None = None
50
+ status: Status | None = None
51
+ assignee: User | None = None
52
+ reporter: User | None = None
53
+ created_at: datetime | None = None
54
+ updated_at: datetime | None = None
55
+ due_date: datetime | None = None
56
+ url: str | None = None
57
+
58
+
59
+ class Comment(BaseModel):
60
+ id: str
61
+ task_id: str
62
+ body: str
63
+ author: User | None = None
64
+ created_at: datetime | None = None
65
+ updated_at: datetime | None = None
66
+
67
+
68
+ class Relation(BaseModel):
69
+ id: str | None = None
70
+ source_task_id: str
71
+ target_task_id: str
72
+ relation_type: RelationType
73
+
74
+
75
+ class TaskQuery(BaseModel):
76
+ project_id: str | None = None
77
+ assignee_id: str | None = None
78
+ status_id: str | None = None
79
+ updated_since: datetime | None = None
80
+ # Soft cap on the number of items the adapter materializes from a paginated
81
+ # provider response. Currently honored by Asana (`list_tasks`); Jira/Yandex
82
+ # ignore it because they already use bounded server-side queries.
83
+ limit: int | None = None
84
+
85
+
86
+ class CreateTaskInput(BaseModel):
87
+ title: str
88
+ description: str | None = None
89
+ project_id: str | None = None
90
+ assignee_id: str | None = None
91
+ status_id: str | None = None
92
+ due_date: datetime | None = None
93
+
94
+
95
+ class UpdateTaskInput(BaseModel):
96
+ title: str | None = None
97
+ description: str | None = None
98
+ assignee_id: str | None = None
99
+ status_id: str | None = None
100
+ due_date: datetime | None = None
101
+
102
+
103
+ class CreateCommentInput(BaseModel):
104
+ task_id: str
105
+ body: str
106
+
107
+
108
+ class CreateProjectInput(BaseModel):
109
+ name: str
110
+ key: str | None = None
111
+ description: str | None = None
112
+ workspace_id: str | None = None
113
+
114
+
115
+ class UpdateProjectInput(BaseModel):
116
+ name: str | None = None
117
+ key: str | None = None
118
+ description: str | None = None
119
+
120
+
121
+ class CreateRelationInput(BaseModel):
122
+ source_task_id: str
123
+ target_task_id: str
124
+ relation_type: RelationType
125
+
126
+
127
+ class UpdateRelationInput(BaseModel):
128
+ source_task_id: str
129
+ target_task_id: str
130
+ relation_type: RelationType
131
+