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,625 @@
1
+ """Deals resource for Megaplan API."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import AsyncIterator
6
+ from typing import Any, overload
7
+
8
+ from megaplan_sdk.constants import ContentType
9
+ from megaplan_sdk.models.comment import Comment
10
+ from megaplan_sdk.models.deal import Deal, DealFullDetails, ProgramState
11
+ from megaplan_sdk.resources.base import BaseResource
12
+ from megaplan_sdk.resources.full_details import FullDetailsMixin, RelatedDataConfig
13
+ from megaplan_sdk.types import FilterType
14
+
15
+
16
+ class DealsResource(BaseResource, FullDetailsMixin):
17
+ """Resource for working with deals."""
18
+
19
+ def __init__(
20
+ self,
21
+ http_client,
22
+ cache=None,
23
+ default_comments_limit: int | None = None,
24
+ default_history_limit: int | None = None,
25
+ ):
26
+ """Initialize deals resource.
27
+
28
+ Args:
29
+ http_client: HTTP client for making requests.
30
+ cache: Optional entity cache.
31
+ default_comments_limit: Default limit for comments in get_full_details().
32
+ default_history_limit: Default limit for history in get_full_details().
33
+ """
34
+ super().__init__(
35
+ http_client,
36
+ cache=cache,
37
+ default_comments_limit=default_comments_limit,
38
+ default_history_limit=default_history_limit,
39
+ )
40
+ # Define config after __init__ to avoid circular import
41
+ self._full_details_config = [
42
+ RelatedDataConfig(
43
+ "comments", "include_comments", "get_comments", limit_param="comments_limit"
44
+ ),
45
+ RelatedDataConfig(
46
+ "history", "include_history", "get_history", limit_param="history_limit"
47
+ ),
48
+ RelatedDataConfig("status_history", "include_status_history", "get_status_history"),
49
+ RelatedDataConfig("auditors", "include_auditors", "get_auditors"),
50
+ RelatedDataConfig(
51
+ "responsible_details",
52
+ "include_responsible_details",
53
+ None,
54
+ entity_field="responsible",
55
+ entity_type="employee",
56
+ ),
57
+ RelatedDataConfig(
58
+ "contractor_details",
59
+ "include_contractor_details",
60
+ None,
61
+ entity_field="contractor",
62
+ entity_type="contractor",
63
+ ),
64
+ RelatedDataConfig(
65
+ "related_tasks",
66
+ "include_related_tasks",
67
+ None,
68
+ custom_fetcher=self._fetch_related_tasks,
69
+ ),
70
+ ]
71
+
72
+ async def _fetch_related_tasks(self, deal_id: int, **kwargs: Any) -> Any:
73
+ """Custom fetcher for related tasks.
74
+
75
+ Note: For tasks API, filter config must be serialized to JSON string
76
+ when it's a dict, because API expects filter as string in query params.
77
+ """
78
+ import json
79
+ from megaplan_sdk.resources.tasks import TasksResource
80
+
81
+ tasks_resource = TasksResource(self._http, cache=self._cache)
82
+ # For tasks API, baseOn must be inside filter config
83
+ # Filter must be JSON string when it's a dict (API requirement)
84
+ filter_config = {"baseOn": {"contentType": ContentType.DEAL, "id": deal_id}}
85
+ # Serialize filter dict to JSON string - API expects string, not object
86
+ filter_str = json.dumps(filter_config, ensure_ascii=False)
87
+ return await tasks_resource.list(filter=filter_str)
88
+
89
+ async def create(self, deal_data: dict[str, Any]) -> Deal:
90
+ """Create a new deal.
91
+
92
+ Args:
93
+ deal_data: Deal data dictionary.
94
+
95
+ Returns:
96
+ Created deal.
97
+ """
98
+ return await self._create_entity("deal", deal_data, Deal)
99
+
100
+ @overload
101
+ async def list(
102
+ self,
103
+ *,
104
+ filter: FilterType | None = None,
105
+ status: ProgramState | None = None,
106
+ q: str | None = None,
107
+ base_on: dict[str, Any] | None = None,
108
+ limit: int | None = None,
109
+ page_after: dict[str, Any] | None = None,
110
+ page_before: dict[str, Any] | None = None,
111
+ page_with: dict[str, Any] | None = None,
112
+ fields: Any | None = None,
113
+ sort_by: list[dict[str, str]] | None = None,
114
+ only_requested_fields: bool | None = None,
115
+ expand: None = None,
116
+ ) -> list[Deal]: ...
117
+
118
+ @overload
119
+ async def list(
120
+ self,
121
+ *,
122
+ filter: FilterType | None = None,
123
+ status: ProgramState | None = None,
124
+ q: str | None = None,
125
+ base_on: dict[str, Any] | None = None,
126
+ limit: int | None = None,
127
+ page_after: dict[str, Any] | None = None,
128
+ page_before: dict[str, Any] | None = None,
129
+ page_with: dict[str, Any] | None = None,
130
+ fields: Any | None = None,
131
+ sort_by: list[dict[str, str]] | None = None,
132
+ only_requested_fields: bool | None = None,
133
+ expand: list[str],
134
+ ) -> list[DealFullDetails]: ...
135
+
136
+ async def list(
137
+ self,
138
+ filter: FilterType | None = None,
139
+ status: ProgramState | None = None,
140
+ q: str | None = None,
141
+ base_on: dict[str, Any] | None = None,
142
+ limit: int | None = None,
143
+ page_after: dict[str, Any] | None = None,
144
+ page_before: dict[str, Any] | None = None,
145
+ page_with: dict[str, Any] | None = None,
146
+ fields: Any | None = None,
147
+ sort_by: list[dict[str, str]] | None = None,
148
+ only_requested_fields: bool | None = None,
149
+ expand: list[str] | None = None,
150
+ ) -> list[Deal] | list[DealFullDetails]:
151
+ """Get list of deals.
152
+
153
+ Args:
154
+ filter: Trade filter (ID or config).
155
+ status: Program state to filter by.
156
+ q: Search query.
157
+ base_on: Base entity for filtering.
158
+ limit: Number of items per page.
159
+ page_after: Load page starting from this entity.
160
+ page_before: Load page strictly before this entity.
161
+ page_with: Load page containing this entity.
162
+ fields: Additional fields to include.
163
+ sort_by: Sort fields.
164
+ only_requested_fields: Return only requested fields.
165
+ expand: List of fields to expand (e.g., ["responsible", "contractor"]).
166
+ Supported values: "responsible", "contractor".
167
+ If provided, returns list[DealFullDetails] instead of list[Deal].
168
+
169
+ Returns:
170
+ List of deals (list[Deal] if expand is None, list[DealFullDetails] otherwise).
171
+
172
+ Examples:
173
+ >>> # Get deals without expansion
174
+ >>> deals = await client.deals.list(limit=10)
175
+ >>>
176
+ >>> # Get deals with expanded responsible and contractor
177
+ >>> deals_full = await client.deals.list(
178
+ ... limit=10, expand=["responsible", "contractor"]
179
+ ... )
180
+ >>> for deal_full in deals_full:
181
+ ... if deal_full.responsible_details:
182
+ ... print(deal_full.responsible_details.display_name())
183
+ ... if deal_full.contractor_details:
184
+ ... print(deal_full.contractor_details.display_name())
185
+ """
186
+ path = self._build_path("api", "v3", "deal")
187
+
188
+ # Prepare deal-specific parameters
189
+ extra_params: dict[str, Any] = {}
190
+ if status:
191
+ extra_params["status"] = (
192
+ status.model_dump(by_alias=True) if hasattr(status, "model_dump") else status
193
+ )
194
+ if q:
195
+ extra_params["q"] = q
196
+ if base_on:
197
+ extra_params["baseOn"] = base_on
198
+
199
+ # Use base method to build params (DRY)
200
+ params = self._build_list_params(
201
+ filter=filter,
202
+ limit=limit,
203
+ page_after=page_after,
204
+ page_before=page_before,
205
+ page_with=page_with,
206
+ fields=fields,
207
+ sort_by=sort_by,
208
+ only_requested_fields=only_requested_fields,
209
+ **extra_params,
210
+ )
211
+
212
+ # 1. Fetch deals
213
+ deals = await self._get_list(path, Deal, params)
214
+
215
+ # 2. If no expand, return as is
216
+ if not expand or not deals:
217
+ return deals
218
+
219
+ # 3. Batch load related entities
220
+ from megaplan_sdk.models.contractor import Contractor
221
+ from megaplan_sdk.models.employee import Employee
222
+
223
+ expand_config: dict[str, tuple[str, type, str]] = {
224
+ "responsible": ("employee", Employee, ContentType.EMPLOYEE),
225
+ "contractor": ("contractor", Contractor, ContentType.CONTRACTOR),
226
+ }
227
+
228
+ expanded = await self._expand_list_entities(deals, expand, expand_config)
229
+ responsible_map = expanded.get("responsible", {})
230
+ contractor_map = expanded.get("contractor", {})
231
+
232
+ # 4. Build DealFullDetails objects
233
+ results = []
234
+ for deal in deals:
235
+ resp_details = None
236
+ contr_details = None
237
+
238
+ if deal.responsible and deal.responsible.id in responsible_map:
239
+ resp_details = responsible_map[deal.responsible.id]
240
+
241
+ if deal.contractor and deal.contractor.id in contractor_map:
242
+ contr_details = contractor_map[deal.contractor.id]
243
+
244
+ results.append(
245
+ DealFullDetails(
246
+ deal=deal,
247
+ responsible_details=resp_details,
248
+ contractor_details=contr_details,
249
+ )
250
+ )
251
+
252
+ return results
253
+
254
+ async def get(self, deal_id: int) -> Deal:
255
+ """Get deal by ID.
256
+
257
+ Args:
258
+ deal_id: Deal identifier.
259
+
260
+ Returns:
261
+ Deal details.
262
+ """
263
+ return await self._get_entity("deal", deal_id, Deal)
264
+
265
+ async def update(self, deal_id: int, deal_data: dict[str, Any]) -> Deal:
266
+ """Update deal.
267
+
268
+ Args:
269
+ deal_id: Deal identifier.
270
+ deal_data: Updated deal data.
271
+
272
+ Returns:
273
+ Updated deal.
274
+ """
275
+ return await self._update_entity("deal", deal_id, deal_data, Deal)
276
+
277
+ async def delete(self, deal_id: int) -> None:
278
+ """Delete deal.
279
+
280
+ Args:
281
+ deal_id: Deal identifier.
282
+ """
283
+ await self._delete_entity("deal", deal_id)
284
+
285
+ async def apply_transition(self, deal_id: int, transition_id: int) -> Deal:
286
+ """Apply transition to deal (change status).
287
+
288
+ Args:
289
+ deal_id: Deal identifier.
290
+ transition_id: Transition identifier.
291
+
292
+ Returns:
293
+ Updated deal.
294
+ """
295
+ path = self._build_path("api", "v3", "deal", str(deal_id), "applyTransition")
296
+ response = await self._http.post(path, json_data={"transition": transition_id})
297
+ return Deal(**response["data"])
298
+
299
+ async def apply_trigger(self, deal_id: int, trigger_id: int) -> Deal:
300
+ """Apply trigger to deal.
301
+
302
+ Args:
303
+ deal_id: Deal identifier.
304
+ trigger_id: Trigger identifier.
305
+
306
+ Returns:
307
+ Updated deal.
308
+ """
309
+ path = self._build_path("api", "v3", "deal", str(deal_id), "applyTrigger")
310
+ response = await self._http.post(path, json_data={"trigger": trigger_id})
311
+ return Deal(**response["data"])
312
+
313
+ async def get_status_history(self, deal_id: int) -> list[dict[str, Any]]:
314
+ """Get status change history for deal.
315
+
316
+ Args:
317
+ deal_id: Deal identifier.
318
+
319
+ Returns:
320
+ List of status history entries.
321
+ """
322
+ path = self._build_path("api", "v3", "deal", str(deal_id), "statusHistory")
323
+ response = await self._http.get(path)
324
+ data: list[dict[str, Any]] = response.get("data", [])
325
+ return data
326
+
327
+ async def check_exists(self, deal_params: dict[str, Any]) -> bool:
328
+ """Check if deal exists.
329
+
330
+ Args:
331
+ deal_params: Parameters to check.
332
+
333
+ Returns:
334
+ True if deal exists, False otherwise.
335
+ """
336
+ path = self._build_path("api", "v3", "deal", "checkDealExist")
337
+ response = await self._http.post(path, json_data=deal_params)
338
+ data: dict[str, Any] = response.get("data", {})
339
+ exists: bool = data.get("exists", False)
340
+ return exists
341
+
342
+ async def iterate(
343
+ self,
344
+ limit: int = 100,
345
+ **kwargs: Any,
346
+ ) -> AsyncIterator[Deal]:
347
+ """Iterate over all deals with automatic pagination.
348
+
349
+ Args:
350
+ limit: Number of items per page.
351
+ **kwargs: Additional parameters to pass to list().
352
+
353
+ Yields:
354
+ Deal objects.
355
+ """
356
+ deal: Deal
357
+ async for deal in self._iterate_generic( # type: ignore[valid-type]
358
+ ContentType.DEAL,
359
+ self.list,
360
+ limit,
361
+ **kwargs,
362
+ ):
363
+ yield deal
364
+
365
+ async def get_comments(
366
+ self,
367
+ deal_id: int,
368
+ limit: int | None = None,
369
+ page_after: dict[str, Any] | None = None,
370
+ page_before: dict[str, Any] | None = None,
371
+ page_with: dict[str, Any] | None = None,
372
+ ) -> list[Comment]:
373
+ """Get comments for a deal.
374
+
375
+ Args:
376
+ deal_id: Deal identifier.
377
+ limit: Number of items per page.
378
+ page_after: Load page starting from this entity.
379
+ page_before: Load page strictly before this entity.
380
+ page_with: Load page containing this entity.
381
+
382
+ Returns:
383
+ List of comments.
384
+
385
+ Examples:
386
+ >>> comments = await client.deals.get_comments(deal_id=123)
387
+ """
388
+ return await self._get_entity_comments(
389
+ "deal",
390
+ deal_id,
391
+ limit,
392
+ page_after,
393
+ page_before,
394
+ page_with,
395
+ )
396
+
397
+ async def create_comment(
398
+ self,
399
+ deal_id: int,
400
+ text: str,
401
+ attaches: list[dict[str, Any]] | None = None,
402
+ ) -> Comment:
403
+ """Create a comment for a deal.
404
+
405
+ Args:
406
+ deal_id: Deal identifier.
407
+ text: Comment text.
408
+ attaches: List of file attachments.
409
+
410
+ Returns:
411
+ Created comment.
412
+
413
+ Examples:
414
+ >>> comment = await client.deals.create_comment(
415
+ ... deal_id=123,
416
+ ... text="Deal update"
417
+ ... )
418
+ """
419
+ return await self._create_entity_comment(
420
+ "deal",
421
+ deal_id,
422
+ text,
423
+ attaches,
424
+ )
425
+
426
+ async def get_auditors(
427
+ self,
428
+ deal_id: int,
429
+ limit: int | None = None,
430
+ page_after: dict[str, Any] | None = None,
431
+ page_before: dict[str, Any] | None = None,
432
+ page_with: dict[str, Any] | None = None,
433
+ ) -> list[dict[str, Any]]:
434
+ """Get auditors for a deal.
435
+
436
+ Args:
437
+ deal_id: Deal identifier.
438
+ limit: Number of items per page.
439
+ page_after: Load page starting from this entity.
440
+ page_before: Load page strictly before this entity.
441
+ page_with: Load page containing this entity.
442
+
443
+ Returns:
444
+ List of auditors.
445
+
446
+ Examples:
447
+ >>> auditors = await client.deals.get_auditors(deal_id=123)
448
+ """
449
+ path = self._build_path("api", "v3", "deal", str(deal_id), "auditors")
450
+
451
+ params = self._build_list_params(
452
+ limit=limit,
453
+ page_after=page_after,
454
+ page_before=page_before,
455
+ page_with=page_with,
456
+ )
457
+
458
+ response = await self._http.get(path, params=params if params else None)
459
+ data: list[dict[str, Any]] = response.get("data", [])
460
+ return data
461
+
462
+ async def add_auditor(
463
+ self,
464
+ deal_id: int,
465
+ auditor_id: int,
466
+ ) -> dict[str, Any]:
467
+ """Add auditor to the deal.
468
+
469
+ Args:
470
+ deal_id: Deal identifier.
471
+ auditor_id: Auditor ID (Employee ID).
472
+
473
+ Returns:
474
+ Added auditor.
475
+
476
+ Examples:
477
+ >>> auditor = await client.deals.add_auditor(
478
+ ... deal_id=123,
479
+ ... auditor_id=456
480
+ ... )
481
+ """
482
+ path = self._build_path("api", "v3", "deal", str(deal_id), "auditors")
483
+ response = await self._http.post(path, json_data={"id": auditor_id})
484
+ result: dict[str, Any] = response.get("data", {})
485
+ return result
486
+
487
+ async def remove_auditor(
488
+ self,
489
+ deal_id: int,
490
+ auditor_id: int,
491
+ ) -> None:
492
+ """Remove auditor from the deal.
493
+
494
+ Args:
495
+ deal_id: Deal identifier.
496
+ auditor_id: Auditor ID.
497
+
498
+ Examples:
499
+ >>> await client.deals.remove_auditor(deal_id=123, auditor_id=456)
500
+ """
501
+ path = self._build_path("api", "v3", "deal", str(deal_id), "auditors", str(auditor_id))
502
+ await self._http.delete(path)
503
+
504
+ async def get_history(
505
+ self,
506
+ deal_id: int,
507
+ limit: int | None = None,
508
+ page_after: dict[str, Any] | None = None,
509
+ page_before: dict[str, Any] | None = None,
510
+ page_with: dict[str, Any] | None = None,
511
+ ) -> list[dict[str, Any]]:
512
+ """Get history log for a deal.
513
+
514
+ Args:
515
+ deal_id: Deal identifier.
516
+ limit: Number of items per page.
517
+ page_after: Load page starting from this entity.
518
+ page_before: Load page strictly before this entity.
519
+ page_with: Load page containing this entity.
520
+
521
+ Returns:
522
+ List of history entries.
523
+
524
+ Examples:
525
+ >>> history = await client.deals.get_history(deal_id=123, limit=10)
526
+ """
527
+ return await self._get_entity_history(
528
+ "deal", deal_id, limit, page_after, page_before, page_with
529
+ )
530
+
531
+ async def search_history(
532
+ self,
533
+ deal_id: int,
534
+ query: str,
535
+ limit: int | None = None,
536
+ page_after: dict[str, Any] | None = None,
537
+ page_before: dict[str, Any] | None = None,
538
+ page_with: dict[str, Any] | None = None,
539
+ ) -> list[dict[str, Any]]:
540
+ """Search in deal history log.
541
+
542
+ Args:
543
+ deal_id: Deal identifier.
544
+ query: Search query.
545
+ limit: Number of items per page.
546
+ page_after: Load page starting from this entity.
547
+ page_before: Load page strictly before this entity.
548
+ page_with: Load page containing this entity.
549
+
550
+ Returns:
551
+ List of matching history entries.
552
+
553
+ Examples:
554
+ >>> results = await client.deals.search_history(deal_id=123, query="transition")
555
+ """
556
+ return await self._search_entity_history(
557
+ "deal", deal_id, query, limit, page_after, page_before, page_with
558
+ )
559
+
560
+ async def get_full_details(
561
+ self,
562
+ deal_id: int,
563
+ include_comments: bool = False,
564
+ include_history: bool = False,
565
+ include_status_history: bool = False,
566
+ include_auditors: bool = False,
567
+ include_responsible_details: bool = False,
568
+ include_contractor_details: bool = False,
569
+ include_related_tasks: bool = False,
570
+ comments_limit: int | None = None,
571
+ history_limit: int | None = None,
572
+ ) -> DealFullDetails:
573
+ """Get full deal details with related entities.
574
+
575
+ This method fetches the deal and optionally loads related data in parallel
576
+ for better performance.
577
+
578
+ Args:
579
+ deal_id: Deal identifier.
580
+ include_comments: Load deal comments.
581
+ include_history: Load change history.
582
+ include_status_history: Load status change history.
583
+ include_auditors: Load auditors list.
584
+ include_responsible_details: Load full responsible (Employee) details.
585
+ include_contractor_details: Load full contractor details.
586
+ include_related_tasks: Load tasks related to this deal.
587
+ comments_limit: Limit for comments (if included).
588
+ None = use global default (from MegaplanClient) or API default.
589
+ Explicit value overrides global default.
590
+ Example: comments_limit=50 returns max 50 comments.
591
+ history_limit: Limit for history (if included).
592
+ None = use global default (from MegaplanClient) or API default.
593
+ Explicit value overrides global default.
594
+ Example: history_limit=100 returns max 100 history entries.
595
+
596
+ Returns:
597
+ DealFullDetails object with all requested data.
598
+
599
+ Examples:
600
+ >>> # Get deal with comments and history
601
+ >>> details = await client.deals.get_full_details(
602
+ ... deal_id=123,
603
+ ... include_comments=True,
604
+ ... include_history=True,
605
+ ... comments_limit=50
606
+ ... )
607
+ >>> print(details.deal.name)
608
+ >>> print(len(details.comments))
609
+ """
610
+ return await self._get_full_details_generic(
611
+ entity_id=deal_id,
612
+ entity_getter="get",
613
+ full_details_class=DealFullDetails,
614
+ config=self._full_details_config,
615
+ main_entity_field="deal",
616
+ include_comments=include_comments,
617
+ include_history=include_history,
618
+ include_status_history=include_status_history,
619
+ include_auditors=include_auditors,
620
+ include_responsible_details=include_responsible_details,
621
+ include_contractor_details=include_contractor_details,
622
+ include_related_tasks=include_related_tasks,
623
+ comments_limit=comments_limit,
624
+ history_limit=history_limit,
625
+ )
@@ -0,0 +1,70 @@
1
+ """Resource for working with departments."""
2
+
3
+ from typing import Any
4
+
5
+ from megaplan_sdk.models.department import Department
6
+ from megaplan_sdk.resources.base import BaseResource
7
+
8
+
9
+ class DepartmentsResource(BaseResource):
10
+ """Resource for working with departments.
11
+
12
+ Provides methods to list and get department information.
13
+
14
+ Examples:
15
+ >>> async with MegaplanClient(...) as client:
16
+ ... # List all departments
17
+ ... departments = await client.departments.list()
18
+ ...
19
+ ... # Get specific department
20
+ ... dept = await client.departments.get(department_id=1)
21
+ """
22
+
23
+ async def list(
24
+ self,
25
+ limit: int | None = None,
26
+ page_after: dict[str, Any] | None = None,
27
+ page_before: dict[str, Any] | None = None,
28
+ page_with: dict[str, Any] | None = None,
29
+ ) -> list[Department]:
30
+ """Get list of departments.
31
+
32
+ Args:
33
+ limit: Maximum number of departments to return.
34
+ page_after: Load page starting from this entity.
35
+ page_before: Load page strictly before this entity.
36
+ page_with: Load page containing this entity.
37
+
38
+ Returns:
39
+ List of departments.
40
+
41
+ Examples:
42
+ >>> async with MegaplanClient(...) as client:
43
+ ... departments = await client.departments.list(limit=50)
44
+ ... for dept in departments:
45
+ ... print(f"{dept.name}")
46
+ """
47
+ path = self._build_path("api", "v3", "department")
48
+ params = self._build_list_params(
49
+ limit=limit,
50
+ page_after=page_after,
51
+ page_before=page_before,
52
+ page_with=page_with,
53
+ )
54
+ return await self._get_list(path, Department, params)
55
+
56
+ async def get(self, department_id: int) -> Department:
57
+ """Get department by ID.
58
+
59
+ Args:
60
+ department_id: Department identifier.
61
+
62
+ Returns:
63
+ Department instance.
64
+
65
+ Examples:
66
+ >>> async with MegaplanClient(...) as client:
67
+ ... dept = await client.departments.get(1)
68
+ ... print(dept.name)
69
+ """
70
+ return await self._get_entity("department", department_id, Department)