megaplan-sdk 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.
- megaplan_sdk/__init__.py +67 -0
- megaplan_sdk/auth.py +185 -0
- megaplan_sdk/cache.py +192 -0
- megaplan_sdk/client.py +201 -0
- megaplan_sdk/constants.py +16 -0
- megaplan_sdk/exceptions.py +180 -0
- megaplan_sdk/helpers.py +108 -0
- megaplan_sdk/http_client.py +390 -0
- megaplan_sdk/logging_config.py +53 -0
- megaplan_sdk/models/__init__.py +22 -0
- megaplan_sdk/models/base.py +16 -0
- megaplan_sdk/models/comment.py +58 -0
- megaplan_sdk/models/common.py +107 -0
- megaplan_sdk/models/contractor.py +137 -0
- megaplan_sdk/models/deal.py +96 -0
- megaplan_sdk/models/department.py +40 -0
- megaplan_sdk/models/employee.py +117 -0
- megaplan_sdk/models/project.py +76 -0
- megaplan_sdk/models/task.py +75 -0
- megaplan_sdk/resources/__init__.py +15 -0
- megaplan_sdk/resources/auth.py +73 -0
- megaplan_sdk/resources/base.py +794 -0
- megaplan_sdk/resources/comments.py +148 -0
- megaplan_sdk/resources/contractors.py +173 -0
- megaplan_sdk/resources/deals.py +625 -0
- megaplan_sdk/resources/departments.py +70 -0
- megaplan_sdk/resources/employees.py +216 -0
- megaplan_sdk/resources/full_details.py +143 -0
- megaplan_sdk/resources/projects.py +854 -0
- megaplan_sdk/resources/tasks.py +932 -0
- megaplan_sdk/types.py +56 -0
- megaplan_sdk-0.1.0.dist-info/METADATA +1383 -0
- megaplan_sdk-0.1.0.dist-info/RECORD +36 -0
- megaplan_sdk-0.1.0.dist-info/WHEEL +5 -0
- megaplan_sdk-0.1.0.dist-info/licenses/LICENSE +21 -0
- megaplan_sdk-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
"""Employees resource for Megaplan API."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import AsyncIterator
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from megaplan_sdk.constants import ContentType
|
|
9
|
+
from megaplan_sdk.models.employee import Employee
|
|
10
|
+
from megaplan_sdk.resources.base import BaseResource
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class EmployeesResource(BaseResource):
|
|
14
|
+
"""Resource for working with employees."""
|
|
15
|
+
|
|
16
|
+
async def create(self, employee_data: dict[str, Any]) -> Employee:
|
|
17
|
+
"""Create a new employee.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
employee_data: Employee data dictionary.
|
|
21
|
+
Required: email, firstName, lastName
|
|
22
|
+
Optional: phone, position, department, etc.
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
Created employee.
|
|
26
|
+
|
|
27
|
+
Examples:
|
|
28
|
+
>>> employee_data = {
|
|
29
|
+
... "email": "user@example.com",
|
|
30
|
+
... "firstName": "John",
|
|
31
|
+
... "lastName": "Doe",
|
|
32
|
+
... "position": "Developer"
|
|
33
|
+
... }
|
|
34
|
+
>>> employee = await client.employees.create(employee_data)
|
|
35
|
+
"""
|
|
36
|
+
return await self._create_entity("employee", employee_data, Employee)
|
|
37
|
+
|
|
38
|
+
async def list(
|
|
39
|
+
self,
|
|
40
|
+
q: str | None = None,
|
|
41
|
+
department_id: int | None = None,
|
|
42
|
+
status: str | None = None,
|
|
43
|
+
limit: int | None = None,
|
|
44
|
+
page_after: dict[str, Any] | None = None,
|
|
45
|
+
page_before: dict[str, Any] | None = None,
|
|
46
|
+
page_with: dict[str, Any] | None = None,
|
|
47
|
+
fields: Any | None = None,
|
|
48
|
+
sort_by: list[dict[str, str]] | None = None,
|
|
49
|
+
only_requested_fields: bool | None = None,
|
|
50
|
+
expand: list[str] | None = None,
|
|
51
|
+
) -> list[Employee]:
|
|
52
|
+
"""Get list of employees.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
q: Search query (name, email, phone).
|
|
56
|
+
NOTE: Search may not work properly in Megaplan API,
|
|
57
|
+
use exact email for best results.
|
|
58
|
+
department_id: Filter by department ID.
|
|
59
|
+
status: Filter by status (active, fired, etc.).
|
|
60
|
+
limit: Number of items per page.
|
|
61
|
+
page_after: Load page starting from this entity.
|
|
62
|
+
page_before: Load page strictly before this entity.
|
|
63
|
+
page_with: Load page containing this entity.
|
|
64
|
+
fields: Additional fields to include.
|
|
65
|
+
sort_by: Sort fields.
|
|
66
|
+
only_requested_fields: Return only requested fields.
|
|
67
|
+
expand: List of fields to expand (e.g., ["department", "manager"]).
|
|
68
|
+
Supported values: "department", "manager".
|
|
69
|
+
Note: When expand is provided, department and manager fields will be
|
|
70
|
+
replaced with full Department/Employee objects instead of BaseEntity.
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
List of employees (with expanded fields if requested).
|
|
74
|
+
|
|
75
|
+
Examples:
|
|
76
|
+
>>> # Get all active employees
|
|
77
|
+
>>> employees = await client.employees.list(status="active")
|
|
78
|
+
>>>
|
|
79
|
+
>>> # Get employees with expanded department
|
|
80
|
+
>>> employees = await client.employees.list(
|
|
81
|
+
... limit=10, expand=["department", "manager"]
|
|
82
|
+
... )
|
|
83
|
+
>>> for employee in employees:
|
|
84
|
+
... if employee.department and hasattr(employee.department, 'name'):
|
|
85
|
+
... print(f"{employee.display_name()} - {employee.department.name}")
|
|
86
|
+
"""
|
|
87
|
+
path = self._build_path("api", "v3", "employee")
|
|
88
|
+
|
|
89
|
+
# Prepare employee-specific parameters
|
|
90
|
+
extra_params: dict[str, Any] = {}
|
|
91
|
+
if q:
|
|
92
|
+
extra_params["q"] = q
|
|
93
|
+
if department_id:
|
|
94
|
+
extra_params["department"] = {
|
|
95
|
+
"id": department_id,
|
|
96
|
+
"contentType": ContentType.DEPARTMENT,
|
|
97
|
+
}
|
|
98
|
+
if status:
|
|
99
|
+
extra_params["status"] = status
|
|
100
|
+
|
|
101
|
+
# Use base method to build params (DRY)
|
|
102
|
+
params = self._build_list_params(
|
|
103
|
+
limit=limit,
|
|
104
|
+
page_after=page_after,
|
|
105
|
+
page_before=page_before,
|
|
106
|
+
page_with=page_with,
|
|
107
|
+
fields=fields,
|
|
108
|
+
sort_by=sort_by,
|
|
109
|
+
only_requested_fields=only_requested_fields,
|
|
110
|
+
**extra_params,
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
# 1. Fetch employees
|
|
114
|
+
employees = await self._get_list(path, Employee, params)
|
|
115
|
+
|
|
116
|
+
# 2. If no expand, return as is
|
|
117
|
+
if not expand or not employees:
|
|
118
|
+
return employees
|
|
119
|
+
|
|
120
|
+
# 3. Batch load related entities
|
|
121
|
+
from megaplan_sdk.models.department import Department
|
|
122
|
+
|
|
123
|
+
expand_config: dict[str, tuple[str, type, str]] = {
|
|
124
|
+
"department": ("department", Department, ContentType.DEPARTMENT),
|
|
125
|
+
"manager": ("employee", Employee, ContentType.EMPLOYEE),
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
expanded = await self._expand_list_entities(employees, expand, expand_config)
|
|
129
|
+
department_map = expanded.get("department", {})
|
|
130
|
+
manager_map = expanded.get("manager", {})
|
|
131
|
+
|
|
132
|
+
# 4. Replace BaseEntity references with full objects
|
|
133
|
+
for employee in employees:
|
|
134
|
+
if employee.department and employee.department.id in department_map:
|
|
135
|
+
employee.department = department_map[employee.department.id]
|
|
136
|
+
|
|
137
|
+
if employee.manager and employee.manager.id in manager_map:
|
|
138
|
+
employee.manager = manager_map[employee.manager.id]
|
|
139
|
+
|
|
140
|
+
return employees
|
|
141
|
+
|
|
142
|
+
async def get(self, employee_id: int) -> Employee:
|
|
143
|
+
"""Get employee by ID.
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
employee_id: Employee identifier.
|
|
147
|
+
|
|
148
|
+
Returns:
|
|
149
|
+
Employee details.
|
|
150
|
+
"""
|
|
151
|
+
return await self._get_entity("employee", employee_id, Employee)
|
|
152
|
+
|
|
153
|
+
async def update(self, employee_id: int, employee_data: dict[str, Any]) -> Employee:
|
|
154
|
+
"""Update employee.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
employee_id: Employee identifier.
|
|
158
|
+
employee_data: Updated employee data.
|
|
159
|
+
|
|
160
|
+
Returns:
|
|
161
|
+
Updated employee.
|
|
162
|
+
"""
|
|
163
|
+
return await self._update_entity("employee", employee_id, employee_data, Employee)
|
|
164
|
+
|
|
165
|
+
async def delete(self, employee_id: int) -> None:
|
|
166
|
+
"""Delete employee.
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
employee_id: Employee identifier.
|
|
170
|
+
"""
|
|
171
|
+
await self._delete_entity("employee", employee_id)
|
|
172
|
+
|
|
173
|
+
async def get_current(self) -> Employee:
|
|
174
|
+
"""Get current authenticated employee.
|
|
175
|
+
|
|
176
|
+
Returns:
|
|
177
|
+
Current employee details.
|
|
178
|
+
|
|
179
|
+
Examples:
|
|
180
|
+
>>> me = await client.employees.get_current()
|
|
181
|
+
>>> print(f"Logged in as: {me.email}")
|
|
182
|
+
|
|
183
|
+
Note:
|
|
184
|
+
Uses /api/v3/currentUser endpoint which returns Employee or ContractorHuman.
|
|
185
|
+
For ContractorHuman users, some fields may be None.
|
|
186
|
+
"""
|
|
187
|
+
path = self._build_path("api", "v3", "currentUser")
|
|
188
|
+
response = await self._http.get(path)
|
|
189
|
+
return Employee(**response["data"])
|
|
190
|
+
|
|
191
|
+
async def iterate(
|
|
192
|
+
self,
|
|
193
|
+
limit: int = 100,
|
|
194
|
+
**kwargs: Any,
|
|
195
|
+
) -> AsyncIterator[Employee]:
|
|
196
|
+
"""Iterate over all employees with automatic pagination.
|
|
197
|
+
|
|
198
|
+
Args:
|
|
199
|
+
limit: Number of items per page.
|
|
200
|
+
**kwargs: Additional parameters to pass to list().
|
|
201
|
+
|
|
202
|
+
Yields:
|
|
203
|
+
Employee objects.
|
|
204
|
+
|
|
205
|
+
Examples:
|
|
206
|
+
>>> async for employee in client.employees.iterate(status="active"):
|
|
207
|
+
... print(f"{employee.first_name} {employee.last_name}")
|
|
208
|
+
"""
|
|
209
|
+
employee: Employee
|
|
210
|
+
async for employee in self._iterate_generic( # type: ignore[valid-type]
|
|
211
|
+
ContentType.EMPLOYEE,
|
|
212
|
+
self.list,
|
|
213
|
+
limit,
|
|
214
|
+
**kwargs,
|
|
215
|
+
):
|
|
216
|
+
yield employee
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"""FullDetailsMixin for unified get_full_details implementation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Awaitable, Callable
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import TYPE_CHECKING, Any
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from pydantic import BaseModel
|
|
11
|
+
|
|
12
|
+
from megaplan_sdk.resources.base import BaseResource
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class RelatedDataConfig:
|
|
17
|
+
"""Configuration for related data loading in get_full_details.
|
|
18
|
+
|
|
19
|
+
Attributes:
|
|
20
|
+
field_name: Name of the field in FullDetails model (e.g., "sub_tasks").
|
|
21
|
+
include_flag: Name of the include_* parameter (e.g., "include_sub_tasks").
|
|
22
|
+
fetch_method: Name of the method to call for loading (e.g., "get_sub_tasks").
|
|
23
|
+
If None, entity_field and entity_type must be provided for entity loading.
|
|
24
|
+
entity_field: Name of the field in main entity to check (e.g., "responsible").
|
|
25
|
+
Used when fetch_method is None.
|
|
26
|
+
entity_type: Entity type for _get_entity_cached (e.g., "employee", "contractor").
|
|
27
|
+
Used when fetch_method is None.
|
|
28
|
+
limit_param: Name of the limit parameter in kwargs (e.g., "comments_limit").
|
|
29
|
+
fetch_args: Additional arguments to pass to fetch_method.
|
|
30
|
+
custom_fetcher: Custom async method to fetch data.
|
|
31
|
+
Bound method that takes (entity_id, **kwargs) and returns coroutine.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
field_name: str
|
|
35
|
+
include_flag: str
|
|
36
|
+
fetch_method: str | None = None
|
|
37
|
+
entity_field: str | None = None
|
|
38
|
+
entity_type: str | None = None
|
|
39
|
+
limit_param: str | None = None
|
|
40
|
+
fetch_args: dict[str, Any] | None = None
|
|
41
|
+
custom_fetcher: Callable[..., Awaitable[Any]] | None = None
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class FullDetailsMixin:
|
|
45
|
+
"""Mixin for implementing get_full_details with configurable related data loading."""
|
|
46
|
+
|
|
47
|
+
async def _get_full_details_generic(
|
|
48
|
+
self: BaseResource,
|
|
49
|
+
entity_id: int,
|
|
50
|
+
entity_getter: str,
|
|
51
|
+
full_details_class: type[BaseModel],
|
|
52
|
+
config: list[RelatedDataConfig],
|
|
53
|
+
main_entity_field: str,
|
|
54
|
+
**kwargs: Any,
|
|
55
|
+
) -> BaseModel:
|
|
56
|
+
"""Generic implementation of get_full_details.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
entity_id: Identifier of the main entity.
|
|
60
|
+
entity_getter: Name of the method to get main entity (e.g., "get").
|
|
61
|
+
full_details_class: FullDetails model class to instantiate.
|
|
62
|
+
config: List of RelatedDataConfig for supported related data.
|
|
63
|
+
main_entity_field: Name of the field in FullDetails for main entity
|
|
64
|
+
(e.g., "task", "project", "deal").
|
|
65
|
+
**kwargs: Parameters from get_full_details call.
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
Instance of full_details_class with all requested data.
|
|
69
|
+
"""
|
|
70
|
+
# Get main entity
|
|
71
|
+
getter = getattr(self, entity_getter)
|
|
72
|
+
main_entity = await getter(entity_id)
|
|
73
|
+
|
|
74
|
+
# Apply global defaults for limit parameters if not explicitly provided
|
|
75
|
+
# Note: parameters are always in kwargs (even if None), so check value instead of presence
|
|
76
|
+
if hasattr(self, "_default_comments_limit") and kwargs.get("comments_limit") is None:
|
|
77
|
+
if self._default_comments_limit is not None:
|
|
78
|
+
kwargs["comments_limit"] = self._default_comments_limit
|
|
79
|
+
|
|
80
|
+
if hasattr(self, "_default_history_limit") and kwargs.get("history_limit") is None:
|
|
81
|
+
if self._default_history_limit is not None:
|
|
82
|
+
kwargs["history_limit"] = self._default_history_limit
|
|
83
|
+
|
|
84
|
+
# Prepare parallel tasks
|
|
85
|
+
tasks: dict[str, Any] = {}
|
|
86
|
+
|
|
87
|
+
for item_config in config:
|
|
88
|
+
include_value = kwargs.get(item_config.include_flag, False)
|
|
89
|
+
if not include_value:
|
|
90
|
+
continue
|
|
91
|
+
|
|
92
|
+
# Handle custom fetcher
|
|
93
|
+
if item_config.custom_fetcher:
|
|
94
|
+
# custom_fetcher is a bound method, call it with entity_id and kwargs
|
|
95
|
+
tasks[item_config.field_name] = item_config.custom_fetcher(entity_id, **kwargs)
|
|
96
|
+
continue
|
|
97
|
+
|
|
98
|
+
# Handle entity loading (responsible, owner, contractor)
|
|
99
|
+
if item_config.fetch_method is None:
|
|
100
|
+
if item_config.entity_field and item_config.entity_type:
|
|
101
|
+
entity_ref = getattr(main_entity, item_config.entity_field, None)
|
|
102
|
+
if entity_ref and hasattr(entity_ref, "id"):
|
|
103
|
+
# Import model class based on entity_type
|
|
104
|
+
if item_config.entity_type == "employee":
|
|
105
|
+
from megaplan_sdk.models.employee import Employee
|
|
106
|
+
|
|
107
|
+
model_class = Employee
|
|
108
|
+
elif item_config.entity_type == "contractor":
|
|
109
|
+
from megaplan_sdk.models.contractor import Contractor
|
|
110
|
+
|
|
111
|
+
model_class = Contractor
|
|
112
|
+
else:
|
|
113
|
+
continue
|
|
114
|
+
|
|
115
|
+
tasks[item_config.field_name] = self._get_entity_cached(
|
|
116
|
+
item_config.entity_type, entity_ref.id, model_class
|
|
117
|
+
)
|
|
118
|
+
continue
|
|
119
|
+
|
|
120
|
+
# Handle method-based loading (lists, etc.)
|
|
121
|
+
fetch_method = getattr(self, item_config.fetch_method)
|
|
122
|
+
fetch_kwargs: dict[str, Any] = {}
|
|
123
|
+
|
|
124
|
+
# Add entity_id as first positional argument
|
|
125
|
+
# Methods like get_sub_tasks(task_id, ...) or get_comments(deal_id, ...)
|
|
126
|
+
if item_config.limit_param and item_config.limit_param in kwargs:
|
|
127
|
+
fetch_kwargs["limit"] = kwargs[item_config.limit_param]
|
|
128
|
+
|
|
129
|
+
if item_config.fetch_args:
|
|
130
|
+
fetch_kwargs.update(item_config.fetch_args)
|
|
131
|
+
|
|
132
|
+
# Call method with entity_id and kwargs
|
|
133
|
+
tasks[item_config.field_name] = fetch_method(entity_id, **fetch_kwargs)
|
|
134
|
+
|
|
135
|
+
# Execute all tasks in parallel
|
|
136
|
+
task_results = await self._fetch_details_parallel(tasks)
|
|
137
|
+
|
|
138
|
+
# Build FullDetails object
|
|
139
|
+
details_kwargs: dict[str, Any] = {main_entity_field: main_entity}
|
|
140
|
+
for item_config in config:
|
|
141
|
+
details_kwargs[item_config.field_name] = task_results.get(item_config.field_name)
|
|
142
|
+
|
|
143
|
+
return full_details_class(**details_kwargs)
|