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,53 @@
1
+ """Logging configuration for Megaplan SDK."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from typing import Any
7
+
8
+ # Create logger for the SDK
9
+ logger = logging.getLogger("megaplan_sdk")
10
+ logger.setLevel(logging.WARNING)
11
+
12
+ # Create formatter
13
+ formatter = logging.Formatter(
14
+ fmt="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
15
+ datefmt="%Y-%m-%d %H:%M:%S",
16
+ )
17
+
18
+ # Create console handler
19
+ console_handler = logging.StreamHandler()
20
+ console_handler.setFormatter(formatter)
21
+ logger.addHandler(console_handler)
22
+
23
+
24
+ def setup_logging(level: str = "WARNING") -> None:
25
+ """Setup logging for SDK.
26
+
27
+ Args:
28
+ level: Logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL).
29
+ """
30
+ logger.setLevel(getattr(logging, level.upper()))
31
+
32
+
33
+ def sanitize_dict(data: dict[str, Any]) -> dict[str, Any]:
34
+ """Remove sensitive data from dict for logging.
35
+
36
+ Args:
37
+ data: Dictionary to sanitize.
38
+
39
+ Returns:
40
+ Sanitized dictionary with sensitive values redacted.
41
+ """
42
+ sensitive_keys = {
43
+ "password",
44
+ "access_token",
45
+ "refresh_token",
46
+ "Authorization",
47
+ "token",
48
+ "secret",
49
+ "api_key",
50
+ "apiKey",
51
+ }
52
+
53
+ return {k: "***REDACTED***" if k in sensitive_keys else v for k, v in data.items()}
@@ -0,0 +1,22 @@
1
+ """Models for Megaplan SDK."""
2
+
3
+ from megaplan_sdk.models.base import BaseEntity
4
+ from megaplan_sdk.models.common import File, Meta, Pagination, SortField
5
+ from megaplan_sdk.models.deal import Deal, ProgramState, TradeFilter
6
+ from megaplan_sdk.models.project import Project, ProjectFilter
7
+ from megaplan_sdk.models.task import Task, TaskFilter
8
+
9
+ __all__ = [
10
+ "BaseEntity",
11
+ "Meta",
12
+ "Pagination",
13
+ "SortField",
14
+ "File",
15
+ "Task",
16
+ "TaskFilter",
17
+ "Project",
18
+ "ProjectFilter",
19
+ "Deal",
20
+ "TradeFilter",
21
+ "ProgramState",
22
+ ]
@@ -0,0 +1,16 @@
1
+ """Base models for Megaplan SDK."""
2
+
3
+ from pydantic import BaseModel, ConfigDict, Field
4
+
5
+
6
+ class BaseEntity(BaseModel):
7
+ """Base entity with contentType and id.
8
+
9
+ All Megaplan entities have contentType and id fields.
10
+ Link entities (for references) only contain these two fields.
11
+ """
12
+
13
+ content_type: str = Field(alias="contentType")
14
+ id: int
15
+
16
+ model_config = ConfigDict(populate_by_name=True, extra="ignore")
@@ -0,0 +1,58 @@
1
+ """Comment model for Megaplan SDK."""
2
+
3
+ from typing import Any
4
+
5
+ from pydantic import ConfigDict, Field
6
+
7
+ from megaplan_sdk.models.base import BaseEntity
8
+ from megaplan_sdk.models.common import TimestampMixin
9
+
10
+
11
+ class Comment(BaseEntity, TimestampMixin):
12
+ """Comment entity.
13
+
14
+ Attributes:
15
+ id: Comment identifier.
16
+ content_type: Entity content type (always "Comment").
17
+ subject: Parent entity (task, project, deal, etc.).
18
+ owner: Comment author (Employee, ContractorCompany, or ContractorHuman).
19
+ content: Comment text content.
20
+ attaches: List of attached files.
21
+ attaches_count: Number of attachments.
22
+ work_time: Work time interval (DateInterval entity with contentType, value).
23
+ work_date: Work date (DateTime entity with contentType, value, timestamp).
24
+ is_unread: Whether comment is unread.
25
+ is_dropped: Whether comment is deleted.
26
+ completed: Completion status.
27
+ created_at: Creation timestamp (timeCreated).
28
+ updated_at: Last update timestamp (timeUpdated).
29
+
30
+ Legacy fields for compatibility:
31
+ author: Alias for owner.
32
+ text: Alias for content.
33
+ """
34
+
35
+ content_type: str = Field(alias="contentType", default="Comment")
36
+ subject: BaseEntity | None = None
37
+ owner: BaseEntity | None = None
38
+ content: str | None = None
39
+ attaches: list[BaseEntity] | None = None
40
+ attaches_count: int | None = Field(alias="attachesCount", default=None)
41
+ work_time: dict[str, Any] | None = Field(alias="workTime", default=None)
42
+ work_date: dict[str, Any] | None = Field(alias="workDate", default=None)
43
+ is_unread: bool | None = Field(alias="isUnread", default=None)
44
+ is_dropped: bool | None = Field(alias="isDropped", default=None)
45
+ completed: int | None = None
46
+
47
+ # Legacy aliases for compatibility
48
+ @property
49
+ def author(self) -> BaseEntity | None:
50
+ """Alias for owner field."""
51
+ return self.owner
52
+
53
+ @property
54
+ def text(self) -> str | None:
55
+ """Alias for content field."""
56
+ return self.content
57
+
58
+ model_config = ConfigDict(populate_by_name=True, extra="ignore")
@@ -0,0 +1,107 @@
1
+ """Common models for Megaplan SDK."""
2
+
3
+ from typing import Any, Generic, TypeVar
4
+
5
+ from pydantic import BaseModel, ConfigDict, Field
6
+
7
+ T = TypeVar("T")
8
+
9
+
10
+ class Pagination(BaseModel):
11
+ """Pagination information from API response.
12
+
13
+ Attributes:
14
+ count: Total number of items.
15
+ limit: Items per page.
16
+ """
17
+
18
+ count: int = 0
19
+ limit: int = 100
20
+
21
+
22
+ class Meta(BaseModel):
23
+ """Meta information from API response.
24
+
25
+ Attributes:
26
+ status: HTTP status code.
27
+ errors: List of error details.
28
+ pagination: Pagination information.
29
+ """
30
+
31
+ status: int = 200
32
+ errors: list[dict[str, Any]] = Field(default_factory=list)
33
+ pagination: Pagination | None = None
34
+
35
+
36
+ class ApiResponse(BaseModel, Generic[T]):
37
+ """Generic API response wrapper.
38
+
39
+ Attributes:
40
+ meta: Meta information (status, errors, pagination).
41
+ data: Response data (single item or list).
42
+ """
43
+
44
+ meta: Meta
45
+ data: T
46
+
47
+
48
+ class File(BaseModel):
49
+ """File model for file uploads and attachments.
50
+
51
+ Attributes:
52
+ id: File identifier.
53
+ content_type: File content type.
54
+ path: File path.
55
+ mime_type: MIME type.
56
+ name: File name.
57
+ size: File size in bytes.
58
+ """
59
+
60
+ id: int
61
+ content_type: str = Field(alias="contentType", default="File")
62
+ path: str | None = None
63
+ mime_type: str | None = Field(alias="mimeType", default=None)
64
+ name: str | None = None
65
+ size: int | None = None
66
+
67
+ model_config = ConfigDict(populate_by_name=True, extra="ignore")
68
+
69
+
70
+ class DateTime(BaseModel):
71
+ """DateTime model for date/time fields.
72
+
73
+ Megaplan API returns dates as objects with contentType and value.
74
+
75
+ Attributes:
76
+ content_type: Always "DateTime".
77
+ value: ISO 8601 datetime string.
78
+ """
79
+
80
+ content_type: str = Field(alias="contentType", default="DateTime")
81
+ value: str
82
+
83
+ model_config = ConfigDict(populate_by_name=True, extra="ignore")
84
+
85
+
86
+ class SortField(BaseModel):
87
+ """Sort field for API requests.
88
+
89
+ Attributes:
90
+ field: Field name to sort by.
91
+ direction: Sort direction (asc or desc).
92
+ """
93
+
94
+ field: str
95
+ direction: str = "asc" # asc or desc
96
+
97
+
98
+ class TimestampMixin(BaseModel):
99
+ """Mixin for entities with creation and update timestamps.
100
+
101
+ Attributes:
102
+ created_at: Entity creation timestamp.
103
+ updated_at: Entity last update timestamp.
104
+ """
105
+
106
+ created_at: str | DateTime | None = Field(alias="createdAt", default=None)
107
+ updated_at: str | DateTime | None = Field(alias="updatedAt", default=None)
@@ -0,0 +1,137 @@
1
+ """Contractor models for Megaplan SDK."""
2
+
3
+ from typing import Any
4
+
5
+ from pydantic import ConfigDict, Field
6
+
7
+ from megaplan_sdk.models.base import BaseEntity
8
+ from megaplan_sdk.models.common import TimestampMixin
9
+
10
+
11
+ class Contractor(BaseEntity, TimestampMixin):
12
+ """Base contractor entity.
13
+
14
+ Attributes:
15
+ id: Contractor identifier.
16
+ content_type: Entity content type (Contractor, ContractorCompany, ContractorHuman).
17
+ name: Contractor name.
18
+ email: Email address.
19
+ phone: Phone number.
20
+ site: Website URL.
21
+ description: Contractor description.
22
+ category: Contractor category.
23
+ manager: Responsible manager.
24
+ created_at: Creation timestamp.
25
+ updated_at: Last update timestamp.
26
+ status: Contractor status.
27
+ tags: List of tags.
28
+ custom_fields: Custom field values.
29
+ """
30
+
31
+ content_type: str = Field(alias="contentType", default="Contractor")
32
+ name: str | None = None
33
+ email: str | None = None
34
+ phone: str | None = None
35
+ site: str | None = None
36
+ description: str | None = None
37
+ category: BaseEntity | None = None
38
+ manager: BaseEntity | None = None
39
+ status: str | None = None
40
+ tags: list[BaseEntity | str] | None = None # Can be Tag entities or strings
41
+ custom_fields: dict[str, Any] | None = Field(alias="customFields", default=None)
42
+
43
+ model_config = ConfigDict(populate_by_name=True, extra="ignore")
44
+
45
+ def display_name(self) -> str:
46
+ """Get display name for contractor.
47
+
48
+ Returns:
49
+ Contractor name or fallback ID representation.
50
+
51
+ Examples:
52
+ >>> contractor = Contractor(id=1, name="ACME Corp")
53
+ >>> contractor.display_name()
54
+ 'ACME Corp'
55
+ """
56
+ return self.name or f"Contractor#{self.id}"
57
+
58
+ def __str__(self) -> str:
59
+ """Return display name for string representation.
60
+
61
+ Returns:
62
+ Display name.
63
+
64
+ Examples:
65
+ >>> contractor = Contractor(id=1, name="ACME Corp")
66
+ >>> str(contractor)
67
+ 'ACME Corp'
68
+ """
69
+ return self.display_name()
70
+
71
+
72
+ class ContractorCompany(Contractor):
73
+ """Company contractor entity.
74
+
75
+ Attributes:
76
+ content_type: Entity content type (always "ContractorCompany").
77
+ inn: Tax identification number (INN).
78
+ kpp: Tax registration reason code (KPP).
79
+ ogrn: Primary state registration number (OGRN).
80
+ legal_name: Legal company name.
81
+ legal_address: Legal address.
82
+ actual_address: Actual address.
83
+ """
84
+
85
+ content_type: str = Field(alias="contentType", default="ContractorCompany")
86
+ inn: str | None = None
87
+ kpp: str | None = None
88
+ ogrn: str | None = None
89
+ legal_name: str | None = Field(alias="legalName", default=None)
90
+ legal_address: str | None = Field(alias="legalAddress", default=None)
91
+ actual_address: str | None = Field(alias="actualAddress", default=None)
92
+
93
+
94
+ class ContractorHuman(Contractor):
95
+ """Human contractor entity (individual).
96
+
97
+ Attributes:
98
+ content_type: Entity content type (always "ContractorHuman").
99
+ first_name: First name.
100
+ middle_name: Middle name.
101
+ last_name: Last name.
102
+ birthday: Birth date.
103
+ passport: Passport information.
104
+ """
105
+
106
+ content_type: str = Field(alias="contentType", default="ContractorHuman")
107
+ first_name: str | None = Field(alias="firstName", default=None)
108
+ middle_name: str | None = Field(alias="middleName", default=None)
109
+ last_name: str | None = Field(alias="lastName", default=None)
110
+ birthday: str | None = None
111
+ passport: str | None = None
112
+
113
+ def display_name(self) -> str:
114
+ """Get display name for human contractor.
115
+
116
+ Returns:
117
+ Full name or name field or fallback ID representation.
118
+
119
+ Examples:
120
+ >>> human = ContractorHuman(
121
+ ... id=1, first_name="John", last_name="Doe"
122
+ ... )
123
+ >>> human.display_name()
124
+ 'John Doe'
125
+ """
126
+ # Try to build from first/last name
127
+ parts = []
128
+ if self.first_name:
129
+ parts.append(self.first_name)
130
+ if self.last_name:
131
+ parts.append(self.last_name)
132
+
133
+ if parts:
134
+ return " ".join(parts)
135
+
136
+ # Fallback to name field or ID
137
+ return self.name or f"Contractor#{self.id}"
@@ -0,0 +1,96 @@
1
+ """Deal models for Megaplan SDK."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from pydantic import BaseModel, ConfigDict, Field
8
+
9
+ from megaplan_sdk.models.base import BaseEntity
10
+ from megaplan_sdk.models.common import DateTime, TimestampMixin
11
+
12
+
13
+ class TradeFilter(BaseModel):
14
+ """Trade filter configuration for deals.
15
+
16
+ Can be used as filter ID (integer) or filter configuration (dict).
17
+ """
18
+
19
+ id: int | None = None
20
+ config: dict[str, Any] | None = None
21
+
22
+
23
+ class ProgramState(BaseModel):
24
+ """Program state model.
25
+
26
+ Represents a state in a deal program.
27
+ """
28
+
29
+ id: int
30
+ content_type: str = Field(alias="contentType", default="ProgramState")
31
+ name: str | None = None
32
+ program: BaseEntity | None = None
33
+
34
+ model_config = ConfigDict(populate_by_name=True, extra="ignore")
35
+
36
+ def __str__(self) -> str:
37
+ """Return state name for display.
38
+
39
+ Returns:
40
+ State name or fallback ID representation.
41
+
42
+ Examples:
43
+ >>> state = ProgramState(id=1, name="In Progress")
44
+ >>> str(state)
45
+ 'In Progress'
46
+ """
47
+ return self.name or f"State#{self.id}"
48
+
49
+
50
+ class Deal(TimestampMixin):
51
+ """Deal model.
52
+
53
+ Represents a deal in Megaplan with all its properties.
54
+ """
55
+
56
+ id: int
57
+ content_type: str = Field(alias="contentType", default="Deal")
58
+ name: str | None = None
59
+ program: BaseEntity | None = None
60
+ state: ProgramState | None = None
61
+ contractor: BaseEntity | None = None
62
+ responsible: BaseEntity | None = None
63
+ sum_base: float | None = Field(alias="sumBase", default=None)
64
+ currency: BaseEntity | None = None
65
+ deadline: str | DateTime | dict[str, Any] | None = None # Can be DateOnly, DateTime, or string
66
+ description: str | None = None
67
+ tags: list[BaseEntity] | None = None
68
+ attaches: list[BaseEntity] | None = None
69
+
70
+ model_config = ConfigDict(populate_by_name=True, extra="ignore")
71
+
72
+
73
+ class DealFullDetails(BaseModel):
74
+ """Full deal details with all related entities.
75
+
76
+ Attributes:
77
+ deal: Main deal entity.
78
+ comments: List of comments (if requested).
79
+ history: Change history entries (if requested).
80
+ status_history: Status change history (if requested).
81
+ auditors: List of auditors (if requested).
82
+ responsible_details: Full responsible employee details (if requested).
83
+ contractor_details: Full contractor details (if requested).
84
+ related_tasks: Tasks related to this deal (if requested).
85
+ """
86
+
87
+ deal: Deal
88
+ comments: list[Any] | None = None
89
+ history: list[dict[str, Any]] | None = None
90
+ status_history: list[dict[str, Any]] | None = None
91
+ auditors: list[dict[str, Any]] | None = None
92
+ responsible_details: Any | None = None
93
+ contractor_details: Any | None = None
94
+ related_tasks: list[Any] | None = None
95
+
96
+ model_config = ConfigDict(populate_by_name=True, extra="ignore")
@@ -0,0 +1,40 @@
1
+ """Department model for Megaplan SDK."""
2
+
3
+ from pydantic import ConfigDict, Field
4
+
5
+ from megaplan_sdk.models.base import BaseEntity
6
+ from megaplan_sdk.models.common import TimestampMixin
7
+
8
+
9
+ class Department(BaseEntity, TimestampMixin):
10
+ """Department entity.
11
+
12
+ Attributes:
13
+ id: Department identifier.
14
+ content_type: Entity content type (always "Department").
15
+ name: Department name.
16
+ parent: Parent department (if nested structure).
17
+ manager: Department manager (Employee reference).
18
+ created_at: Creation timestamp.
19
+ updated_at: Last update timestamp.
20
+ """
21
+
22
+ content_type: str = Field(alias="contentType", default="Department")
23
+ name: str | None = None
24
+ parent: BaseEntity | None = None
25
+ manager: BaseEntity | None = None
26
+
27
+ model_config = ConfigDict(populate_by_name=True, extra="ignore")
28
+
29
+ def __str__(self) -> str:
30
+ """Return department name for display.
31
+
32
+ Returns:
33
+ Department name or fallback ID representation.
34
+
35
+ Examples:
36
+ >>> dept = Department(id=1, name="Engineering")
37
+ >>> str(dept)
38
+ 'Engineering'
39
+ """
40
+ return self.name or f"Department#{self.id}"
@@ -0,0 +1,117 @@
1
+ """Employee model for Megaplan SDK."""
2
+
3
+ from typing import Any
4
+
5
+ from pydantic import ConfigDict, Field
6
+
7
+ from megaplan_sdk.models.base import BaseEntity
8
+ from megaplan_sdk.models.common import DateTime, TimestampMixin
9
+
10
+
11
+ class Employee(BaseEntity, TimestampMixin):
12
+ """Employee entity.
13
+
14
+ Attributes:
15
+ id: Employee identifier.
16
+ content_type: Entity content type (always "Employee").
17
+ first_name: First name.
18
+ middle_name: Middle name.
19
+ last_name: Last name.
20
+ email: Email address.
21
+ phone: Phone number.
22
+ position: Job position.
23
+ department: Department entity.
24
+ manager: Direct manager entity.
25
+ birthday: Birth date (DateOnly entity with year, month, day fields).
26
+ hired_at: Hire date.
27
+ fired_at: Termination date.
28
+ status: Employment status (EmployeeStatus entity with name, masterType).
29
+ avatar: Avatar image entity.
30
+ is_admin: Whether employee has admin rights.
31
+ is_client: Whether this is a client account.
32
+ access_role: Access role entity.
33
+ created_at: Creation timestamp.
34
+ updated_at: Last update timestamp.
35
+ custom_fields: Custom field values.
36
+ """
37
+
38
+ content_type: str = Field(alias="contentType", default="Employee")
39
+ first_name: str | None = Field(alias="firstName", default=None)
40
+ middle_name: str | None = Field(alias="middleName", default=None)
41
+ last_name: str | None = Field(alias="lastName", default=None)
42
+ email: str | None = None
43
+ phone: str | None = None
44
+ position: str | None = None
45
+ department: BaseEntity | None = None
46
+ manager: BaseEntity | None = None
47
+ birthday: dict[str, Any] | None = None # DateOnly entity (year, month, day)
48
+ hired_at: str | DateTime | None = Field(alias="hiredAt", default=None)
49
+ fired_at: str | DateTime | None = Field(alias="firedAt", default=None)
50
+ status: BaseEntity | None = None # EmployeeStatus entity
51
+ avatar: BaseEntity | None = None
52
+ is_admin: bool | None = Field(alias="isAdmin", default=None)
53
+ is_client: bool | None = Field(alias="isClient", default=None)
54
+ access_role: BaseEntity | None = Field(alias="accessRole", default=None)
55
+ custom_fields: dict[str, Any] | None = Field(alias="customFields", default=None)
56
+
57
+ model_config = ConfigDict(populate_by_name=True, extra="ignore")
58
+
59
+ def full_name(self, include_middle: bool = True) -> str:
60
+ """Get full name of the employee.
61
+
62
+ Args:
63
+ include_middle: Include middle name (default: True).
64
+
65
+ Returns:
66
+ Full name string.
67
+
68
+ Examples:
69
+ >>> employee = Employee(
70
+ ... id=1, first_name="John", middle_name="A", last_name="Doe"
71
+ ... )
72
+ >>> employee.full_name()
73
+ 'John A Doe'
74
+ >>> employee.full_name(include_middle=False)
75
+ 'John Doe'
76
+ """
77
+ parts = []
78
+ if self.first_name:
79
+ parts.append(self.first_name)
80
+ if include_middle and self.middle_name:
81
+ parts.append(self.middle_name)
82
+ if self.last_name:
83
+ parts.append(self.last_name)
84
+ return " ".join(parts) if parts else f"Employee#{self.id}"
85
+
86
+ def display_name(self) -> str:
87
+ """Get display name with position.
88
+
89
+ Returns:
90
+ Full name with position in parentheses.
91
+
92
+ Examples:
93
+ >>> employee = Employee(
94
+ ... id=1, first_name="John", last_name="Doe", position="CEO"
95
+ ... )
96
+ >>> employee.display_name()
97
+ 'John Doe (CEO)'
98
+ """
99
+ name = self.full_name(include_middle=False)
100
+ if self.position:
101
+ return f"{name} ({self.position})"
102
+ return name
103
+
104
+ def __str__(self) -> str:
105
+ """Return display name for string representation.
106
+
107
+ Returns:
108
+ Display name with position.
109
+
110
+ Examples:
111
+ >>> employee = Employee(
112
+ ... id=1, first_name="John", last_name="Doe", position="CEO"
113
+ ... )
114
+ >>> str(employee)
115
+ 'John Doe (CEO)'
116
+ """
117
+ return self.display_name()