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.
@@ -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)