megaplan-sdk 0.2.3__tar.gz → 0.3.0__tar.gz
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-0.2.3/src/megaplan_sdk.egg-info → megaplan_sdk-0.3.0}/PKG-INFO +41 -1
- {megaplan_sdk-0.2.3 → megaplan_sdk-0.3.0}/README.md +40 -0
- {megaplan_sdk-0.2.3 → megaplan_sdk-0.3.0}/pyproject.toml +1 -1
- {megaplan_sdk-0.2.3 → megaplan_sdk-0.3.0}/src/megaplan_sdk/__init__.py +9 -1
- megaplan_sdk-0.3.0/src/megaplan_sdk/_knowledge_links.py +26 -0
- {megaplan_sdk-0.2.3 → megaplan_sdk-0.3.0}/src/megaplan_sdk/client.py +6 -0
- {megaplan_sdk-0.2.3 → megaplan_sdk-0.3.0}/src/megaplan_sdk/constants.py +2 -0
- {megaplan_sdk-0.2.3 → megaplan_sdk-0.3.0}/src/megaplan_sdk/models/__init__.py +8 -0
- megaplan_sdk-0.3.0/src/megaplan_sdk/models/knowledge.py +56 -0
- {megaplan_sdk-0.2.3 → megaplan_sdk-0.3.0}/src/megaplan_sdk/resources/base.py +2 -0
- megaplan_sdk-0.3.0/src/megaplan_sdk/resources/knowledge_article.py +28 -0
- megaplan_sdk-0.3.0/src/megaplan_sdk/resources/knowledge_base.py +149 -0
- {megaplan_sdk-0.2.3 → megaplan_sdk-0.3.0/src/megaplan_sdk.egg-info}/PKG-INFO +41 -1
- {megaplan_sdk-0.2.3 → megaplan_sdk-0.3.0}/src/megaplan_sdk.egg-info/SOURCES.txt +4 -0
- {megaplan_sdk-0.2.3 → megaplan_sdk-0.3.0}/LICENSE +0 -0
- {megaplan_sdk-0.2.3 → megaplan_sdk-0.3.0}/setup.cfg +0 -0
- {megaplan_sdk-0.2.3 → megaplan_sdk-0.3.0}/src/megaplan_sdk/auth.py +0 -0
- {megaplan_sdk-0.2.3 → megaplan_sdk-0.3.0}/src/megaplan_sdk/cache.py +0 -0
- {megaplan_sdk-0.2.3 → megaplan_sdk-0.3.0}/src/megaplan_sdk/exceptions.py +0 -0
- {megaplan_sdk-0.2.3 → megaplan_sdk-0.3.0}/src/megaplan_sdk/filter_builder.py +0 -0
- {megaplan_sdk-0.2.3 → megaplan_sdk-0.3.0}/src/megaplan_sdk/helpers.py +0 -0
- {megaplan_sdk-0.2.3 → megaplan_sdk-0.3.0}/src/megaplan_sdk/http_client.py +0 -0
- {megaplan_sdk-0.2.3 → megaplan_sdk-0.3.0}/src/megaplan_sdk/logging_config.py +0 -0
- {megaplan_sdk-0.2.3 → megaplan_sdk-0.3.0}/src/megaplan_sdk/models/base.py +0 -0
- {megaplan_sdk-0.2.3 → megaplan_sdk-0.3.0}/src/megaplan_sdk/models/comment.py +0 -0
- {megaplan_sdk-0.2.3 → megaplan_sdk-0.3.0}/src/megaplan_sdk/models/common.py +0 -0
- {megaplan_sdk-0.2.3 → megaplan_sdk-0.3.0}/src/megaplan_sdk/models/contractor.py +0 -0
- {megaplan_sdk-0.2.3 → megaplan_sdk-0.3.0}/src/megaplan_sdk/models/deal.py +0 -0
- {megaplan_sdk-0.2.3 → megaplan_sdk-0.3.0}/src/megaplan_sdk/models/department.py +0 -0
- {megaplan_sdk-0.2.3 → megaplan_sdk-0.3.0}/src/megaplan_sdk/models/employee.py +0 -0
- {megaplan_sdk-0.2.3 → megaplan_sdk-0.3.0}/src/megaplan_sdk/models/filter.py +0 -0
- {megaplan_sdk-0.2.3 → megaplan_sdk-0.3.0}/src/megaplan_sdk/models/group.py +0 -0
- {megaplan_sdk-0.2.3 → megaplan_sdk-0.3.0}/src/megaplan_sdk/models/milestone.py +0 -0
- {megaplan_sdk-0.2.3 → megaplan_sdk-0.3.0}/src/megaplan_sdk/models/participant.py +0 -0
- {megaplan_sdk-0.2.3 → megaplan_sdk-0.3.0}/src/megaplan_sdk/models/project.py +0 -0
- {megaplan_sdk-0.2.3 → megaplan_sdk-0.3.0}/src/megaplan_sdk/models/task.py +0 -0
- {megaplan_sdk-0.2.3 → megaplan_sdk-0.3.0}/src/megaplan_sdk/resources/__init__.py +0 -0
- {megaplan_sdk-0.2.3 → megaplan_sdk-0.3.0}/src/megaplan_sdk/resources/auth.py +0 -0
- {megaplan_sdk-0.2.3 → megaplan_sdk-0.3.0}/src/megaplan_sdk/resources/comments.py +0 -0
- {megaplan_sdk-0.2.3 → megaplan_sdk-0.3.0}/src/megaplan_sdk/resources/contractors.py +0 -0
- {megaplan_sdk-0.2.3 → megaplan_sdk-0.3.0}/src/megaplan_sdk/resources/deals.py +0 -0
- {megaplan_sdk-0.2.3 → megaplan_sdk-0.3.0}/src/megaplan_sdk/resources/departments.py +0 -0
- {megaplan_sdk-0.2.3 → megaplan_sdk-0.3.0}/src/megaplan_sdk/resources/employees.py +0 -0
- {megaplan_sdk-0.2.3 → megaplan_sdk-0.3.0}/src/megaplan_sdk/resources/filters.py +0 -0
- {megaplan_sdk-0.2.3 → megaplan_sdk-0.3.0}/src/megaplan_sdk/resources/full_details.py +0 -0
- {megaplan_sdk-0.2.3 → megaplan_sdk-0.3.0}/src/megaplan_sdk/resources/projects.py +0 -0
- {megaplan_sdk-0.2.3 → megaplan_sdk-0.3.0}/src/megaplan_sdk/resources/tasks.py +0 -0
- {megaplan_sdk-0.2.3 → megaplan_sdk-0.3.0}/src/megaplan_sdk/types.py +0 -0
- {megaplan_sdk-0.2.3 → megaplan_sdk-0.3.0}/src/megaplan_sdk.egg-info/dependency_links.txt +0 -0
- {megaplan_sdk-0.2.3 → megaplan_sdk-0.3.0}/src/megaplan_sdk.egg-info/requires.txt +0 -0
- {megaplan_sdk-0.2.3 → megaplan_sdk-0.3.0}/src/megaplan_sdk.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: megaplan-sdk
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Professional Python SDK for Megaplan API v3
|
|
5
5
|
Author-email: Maxim Borzov <max@borzov.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -67,6 +67,7 @@ Dynamic: license-file
|
|
|
67
67
|
- [Задачи](#работа-с-задачами)
|
|
68
68
|
- [Проекты](#работа-с-проектами)
|
|
69
69
|
- [Сделки](#работа-со-сделками)
|
|
70
|
+
- [База знаний](#работа-с-базой-знаний)
|
|
70
71
|
|
|
71
72
|
### Продвинутые возможности
|
|
72
73
|
- [Кэширование сущностей](#кэширование-сущностей)
|
|
@@ -1123,6 +1124,45 @@ details = await client.deals.get_full_details(
|
|
|
1123
1124
|
|
|
1124
1125
|
> **Примечание:** Общее описание метода `get_full_details()` и примеры использования см. в разделе [Общие паттерны работы с сущностями](#общие-паттерны-работы-с-сущностями).
|
|
1125
1126
|
|
|
1127
|
+
## Работа с базой знаний
|
|
1128
|
+
|
|
1129
|
+
SDK предоставляет доступ к разделам и статьям Базы знаний Мегаплана через два ресурса: `client.knowledge_base` (разделы) и `client.knowledge_article` (статьи).
|
|
1130
|
+
|
|
1131
|
+
```python
|
|
1132
|
+
# Список разделов Базы знаний (плоский)
|
|
1133
|
+
sections = await client.knowledge_base.list()
|
|
1134
|
+
for s in sections:
|
|
1135
|
+
print(s.id, s.name)
|
|
1136
|
+
|
|
1137
|
+
# Один раздел с HTML-содержимым
|
|
1138
|
+
section = await client.knowledge_base.get(11)
|
|
1139
|
+
print(section.content)
|
|
1140
|
+
|
|
1141
|
+
# Итерация по всем разделам с автопагинацией
|
|
1142
|
+
async for section in client.knowledge_base.iterate():
|
|
1143
|
+
print(section.id, section.name)
|
|
1144
|
+
|
|
1145
|
+
# Статья по ID (parent всегда None — используйте base)
|
|
1146
|
+
article = await client.knowledge_article.get(33)
|
|
1147
|
+
print(article.name, "→ раздел:", article.base.name if article.base else None)
|
|
1148
|
+
|
|
1149
|
+
# Экспериментально: раздел вместе со статьями (через парсинг HTML-ссылок)
|
|
1150
|
+
bundle = await client.knowledge_base.get_with_articles(2)
|
|
1151
|
+
for a in bundle.articles:
|
|
1152
|
+
print(a.id, a.name)
|
|
1153
|
+
```
|
|
1154
|
+
|
|
1155
|
+
> **Известные ограничения API (серверная сторона):**
|
|
1156
|
+
>
|
|
1157
|
+
> - **Нет листинга статей:** эндпоинт `GET /api/v3/knowledgeArticle` отсутствует (возвращает 404).
|
|
1158
|
+
> Единственный способ обнаружения статей в разделе — метод `get_with_articles()`, который
|
|
1159
|
+
> парсит HTML-ссылки из поля `content` раздела. Это **экспериментальный** и хрупкий метод —
|
|
1160
|
+
> формат ссылок может измениться на стороне сервера.
|
|
1161
|
+
> - **Фильтр `parent` не работает:** `knowledge_base.list()` всегда возвращает плоский список
|
|
1162
|
+
> всех разделов; передача `parent` в фильтре игнорируется сервером. Иерархии разделов нет.
|
|
1163
|
+
> - **Поле `parent` у статьи всегда `null`:** для определения принадлежности статьи к разделу
|
|
1164
|
+
> используйте `article.base` (не `article.parent`).
|
|
1165
|
+
|
|
1126
1166
|
## Продвинутые возможности
|
|
1127
1167
|
|
|
1128
1168
|
### Кэширование сущностей
|
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
- [Задачи](#работа-с-задачами)
|
|
36
36
|
- [Проекты](#работа-с-проектами)
|
|
37
37
|
- [Сделки](#работа-со-сделками)
|
|
38
|
+
- [База знаний](#работа-с-базой-знаний)
|
|
38
39
|
|
|
39
40
|
### Продвинутые возможности
|
|
40
41
|
- [Кэширование сущностей](#кэширование-сущностей)
|
|
@@ -1091,6 +1092,45 @@ details = await client.deals.get_full_details(
|
|
|
1091
1092
|
|
|
1092
1093
|
> **Примечание:** Общее описание метода `get_full_details()` и примеры использования см. в разделе [Общие паттерны работы с сущностями](#общие-паттерны-работы-с-сущностями).
|
|
1093
1094
|
|
|
1095
|
+
## Работа с базой знаний
|
|
1096
|
+
|
|
1097
|
+
SDK предоставляет доступ к разделам и статьям Базы знаний Мегаплана через два ресурса: `client.knowledge_base` (разделы) и `client.knowledge_article` (статьи).
|
|
1098
|
+
|
|
1099
|
+
```python
|
|
1100
|
+
# Список разделов Базы знаний (плоский)
|
|
1101
|
+
sections = await client.knowledge_base.list()
|
|
1102
|
+
for s in sections:
|
|
1103
|
+
print(s.id, s.name)
|
|
1104
|
+
|
|
1105
|
+
# Один раздел с HTML-содержимым
|
|
1106
|
+
section = await client.knowledge_base.get(11)
|
|
1107
|
+
print(section.content)
|
|
1108
|
+
|
|
1109
|
+
# Итерация по всем разделам с автопагинацией
|
|
1110
|
+
async for section in client.knowledge_base.iterate():
|
|
1111
|
+
print(section.id, section.name)
|
|
1112
|
+
|
|
1113
|
+
# Статья по ID (parent всегда None — используйте base)
|
|
1114
|
+
article = await client.knowledge_article.get(33)
|
|
1115
|
+
print(article.name, "→ раздел:", article.base.name if article.base else None)
|
|
1116
|
+
|
|
1117
|
+
# Экспериментально: раздел вместе со статьями (через парсинг HTML-ссылок)
|
|
1118
|
+
bundle = await client.knowledge_base.get_with_articles(2)
|
|
1119
|
+
for a in bundle.articles:
|
|
1120
|
+
print(a.id, a.name)
|
|
1121
|
+
```
|
|
1122
|
+
|
|
1123
|
+
> **Известные ограничения API (серверная сторона):**
|
|
1124
|
+
>
|
|
1125
|
+
> - **Нет листинга статей:** эндпоинт `GET /api/v3/knowledgeArticle` отсутствует (возвращает 404).
|
|
1126
|
+
> Единственный способ обнаружения статей в разделе — метод `get_with_articles()`, который
|
|
1127
|
+
> парсит HTML-ссылки из поля `content` раздела. Это **экспериментальный** и хрупкий метод —
|
|
1128
|
+
> формат ссылок может измениться на стороне сервера.
|
|
1129
|
+
> - **Фильтр `parent` не работает:** `knowledge_base.list()` всегда возвращает плоский список
|
|
1130
|
+
> всех разделов; передача `parent` в фильтре игнорируется сервером. Иерархии разделов нет.
|
|
1131
|
+
> - **Поле `parent` у статьи всегда `null`:** для определения принадлежности статьи к разделу
|
|
1132
|
+
> используйте `article.base` (не `article.parent`).
|
|
1133
|
+
|
|
1094
1134
|
## Продвинутые возможности
|
|
1095
1135
|
|
|
1096
1136
|
### Кэширование сущностей
|
|
@@ -42,6 +42,11 @@ from megaplan_sdk.models.filter import (
|
|
|
42
42
|
UserSetting,
|
|
43
43
|
)
|
|
44
44
|
from megaplan_sdk.models.group import Group
|
|
45
|
+
from megaplan_sdk.models.knowledge import (
|
|
46
|
+
KnowledgeArticle,
|
|
47
|
+
KnowledgeBase,
|
|
48
|
+
KnowledgeSectionWithArticles,
|
|
49
|
+
)
|
|
45
50
|
from megaplan_sdk.models.milestone import Milestone
|
|
46
51
|
from megaplan_sdk.models.participant import Participant, parse_participant, parse_participants
|
|
47
52
|
from megaplan_sdk.models.project import Project, ProjectFullDetails
|
|
@@ -75,6 +80,9 @@ __all__ = [
|
|
|
75
80
|
"Department",
|
|
76
81
|
"Group",
|
|
77
82
|
"Milestone",
|
|
83
|
+
"KnowledgeBase",
|
|
84
|
+
"KnowledgeArticle",
|
|
85
|
+
"KnowledgeSectionWithArticles",
|
|
78
86
|
"Participant",
|
|
79
87
|
"parse_participant",
|
|
80
88
|
"parse_participants",
|
|
@@ -106,4 +114,4 @@ __all__ = [
|
|
|
106
114
|
"DEFAULT_TASK_LIST_FIELDS",
|
|
107
115
|
]
|
|
108
116
|
|
|
109
|
-
__version__ = "0.
|
|
117
|
+
__version__ = "0.3.0"
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""HTML link parsing for Knowledge Base article discovery.
|
|
2
|
+
|
|
3
|
+
EXPERIMENTAL / FRAGILE: this module is the single place coupled to Megaplan's
|
|
4
|
+
HTML format. The Megaplan API has no endpoint to list articles in a section,
|
|
5
|
+
so article IDs are discovered by parsing ``/knowledge/<section>/<article>``
|
|
6
|
+
links inside a section's HTML ``content``.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import re
|
|
10
|
+
|
|
11
|
+
# Matches /knowledge/<section_id> and /knowledge/<section_id>/<article_id>
|
|
12
|
+
# inside href values; scheme/host-agnostic (absolute or relative URLs).
|
|
13
|
+
_KNOWLEDGE_LINK_RE = re.compile(r"/knowledge/(\d+)(?:/(\d+))?")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def extract_article_ids(html: str | None) -> list[int]:
|
|
17
|
+
"""Extract article IDs from two-segment ``/knowledge/<section>/<article>`` links.
|
|
18
|
+
|
|
19
|
+
Single-segment ``/knowledge/<section>`` links (section references, e.g. a
|
|
20
|
+
table of contents) are ignored. Returns unique IDs in first-seen order.
|
|
21
|
+
"""
|
|
22
|
+
seen: dict[int, None] = {}
|
|
23
|
+
for _section, article in _KNOWLEDGE_LINK_RE.findall(html or ""):
|
|
24
|
+
if article:
|
|
25
|
+
seen.setdefault(int(article), None)
|
|
26
|
+
return list(seen)
|
|
@@ -13,6 +13,8 @@ from megaplan_sdk.resources.deals import DealsResource
|
|
|
13
13
|
from megaplan_sdk.resources.departments import DepartmentsResource
|
|
14
14
|
from megaplan_sdk.resources.employees import EmployeesResource
|
|
15
15
|
from megaplan_sdk.resources.filters import FiltersResource
|
|
16
|
+
from megaplan_sdk.resources.knowledge_article import KnowledgeArticleResource
|
|
17
|
+
from megaplan_sdk.resources.knowledge_base import KnowledgeBaseResource
|
|
16
18
|
from megaplan_sdk.resources.projects import ProjectsResource
|
|
17
19
|
from megaplan_sdk.resources.tasks import TasksResource
|
|
18
20
|
|
|
@@ -119,6 +121,10 @@ class MegaplanClient:
|
|
|
119
121
|
self.employees = EmployeesResource(self._http, cache=self._cache)
|
|
120
122
|
self.departments = DepartmentsResource(self._http, cache=self._cache)
|
|
121
123
|
self.filters = FiltersResource(self._http, cache=self._cache)
|
|
124
|
+
self.knowledge_article = KnowledgeArticleResource(self._http, cache=self._cache)
|
|
125
|
+
self.knowledge_base = KnowledgeBaseResource(
|
|
126
|
+
self._http, cache=self._cache, article_resource=self.knowledge_article
|
|
127
|
+
)
|
|
122
128
|
|
|
123
129
|
# Security: Store password only for initial authentication if provided
|
|
124
130
|
self._initial_password = password if (username and password) else None
|
|
@@ -4,6 +4,11 @@ from megaplan_sdk.models.base import BaseEntity
|
|
|
4
4
|
from megaplan_sdk.models.common import File, Meta, Pagination, SortField
|
|
5
5
|
from megaplan_sdk.models.deal import Deal, ProgramState, TradeFilter
|
|
6
6
|
from megaplan_sdk.models.group import Group
|
|
7
|
+
from megaplan_sdk.models.knowledge import (
|
|
8
|
+
KnowledgeArticle,
|
|
9
|
+
KnowledgeBase,
|
|
10
|
+
KnowledgeSectionWithArticles,
|
|
11
|
+
)
|
|
7
12
|
from megaplan_sdk.models.participant import Participant, parse_participant, parse_participants
|
|
8
13
|
from megaplan_sdk.models.project import Project, ProjectFilter
|
|
9
14
|
from megaplan_sdk.models.task import Task, TaskFilter
|
|
@@ -25,4 +30,7 @@ __all__ = [
|
|
|
25
30
|
"Participant",
|
|
26
31
|
"parse_participant",
|
|
27
32
|
"parse_participants",
|
|
33
|
+
"KnowledgeBase",
|
|
34
|
+
"KnowledgeArticle",
|
|
35
|
+
"KnowledgeSectionWithArticles",
|
|
28
36
|
]
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""Knowledge Base models for Megaplan SDK."""
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
4
|
+
|
|
5
|
+
from megaplan_sdk.models.base import BaseEntity
|
|
6
|
+
from megaplan_sdk.models.common import DateTime
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class KnowledgeBase(BaseEntity):
|
|
10
|
+
"""Knowledge Base section.
|
|
11
|
+
|
|
12
|
+
The list endpoint returns a trimmed object (no ``content`` /
|
|
13
|
+
``last_updated`` / ``is_dropped``); ``knowledge_base.get(id)`` returns
|
|
14
|
+
the full object including the HTML ``content``.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
content_type: str = Field(alias="contentType", default="KnowledgeBase")
|
|
18
|
+
content: str | None = None # HTML, present only in the full get() response
|
|
19
|
+
access_role: str | None = Field(alias="accessRole", default=None)
|
|
20
|
+
last_updated: DateTime | None = Field(alias="lastUpdated", default=None)
|
|
21
|
+
last_update_by: BaseEntity | None = Field(alias="lastUpdateBy", default=None)
|
|
22
|
+
order_pos: int | None = Field(alias="orderPos", default=None)
|
|
23
|
+
expanded: bool | None = None
|
|
24
|
+
is_dropped: bool | None = Field(alias="isDropped", default=None)
|
|
25
|
+
|
|
26
|
+
model_config = ConfigDict(populate_by_name=True, extra="allow")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class KnowledgeArticle(BaseEntity):
|
|
30
|
+
"""Knowledge Base article.
|
|
31
|
+
|
|
32
|
+
NOTE: ``parent`` is ALWAYS null in the Megaplan API — use ``base`` (the
|
|
33
|
+
parent section reference) to determine which section the article belongs to.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
content_type: str = Field(alias="contentType", default="KnowledgeArticle")
|
|
37
|
+
content: str | None = None # HTML body
|
|
38
|
+
parent: BaseEntity | None = None # always null in API; use `base`
|
|
39
|
+
base: KnowledgeBase | None = None # the real parent-section link
|
|
40
|
+
access_role: str | None = Field(alias="accessRole", default=None)
|
|
41
|
+
last_updated: DateTime | None = Field(alias="lastUpdated", default=None)
|
|
42
|
+
last_update_by: BaseEntity | None = Field(alias="lastUpdateBy", default=None)
|
|
43
|
+
order_pos: int | None = Field(alias="orderPos", default=None)
|
|
44
|
+
expanded: bool | None = None
|
|
45
|
+
is_dropped: bool | None = Field(alias="isDropped", default=None)
|
|
46
|
+
|
|
47
|
+
model_config = ConfigDict(populate_by_name=True, extra="allow")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class KnowledgeSectionWithArticles(BaseModel):
|
|
51
|
+
"""Composite returned by the experimental ``get_with_articles`` helper."""
|
|
52
|
+
|
|
53
|
+
section: KnowledgeBase
|
|
54
|
+
articles: list[KnowledgeArticle]
|
|
55
|
+
|
|
56
|
+
model_config = ConfigDict(populate_by_name=True, extra="allow")
|
|
@@ -521,6 +521,8 @@ class BaseResource:
|
|
|
521
521
|
"contractorCompany": ContentType.CONTRACTOR_COMPANY,
|
|
522
522
|
"contractorHuman": ContentType.CONTRACTOR_HUMAN,
|
|
523
523
|
"comment": ContentType.COMMENT,
|
|
524
|
+
"knowledgeBase": ContentType.KNOWLEDGE_BASE,
|
|
525
|
+
"knowledgeArticle": ContentType.KNOWLEDGE_ARTICLE,
|
|
524
526
|
}
|
|
525
527
|
|
|
526
528
|
result = mapping.get(entity_type)
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Knowledge Base article resource for Megaplan API."""
|
|
2
|
+
|
|
3
|
+
from megaplan_sdk.models.knowledge import KnowledgeArticle
|
|
4
|
+
from megaplan_sdk.resources.base import BaseResource
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class KnowledgeArticleResource(BaseResource):
|
|
8
|
+
"""Resource for Knowledge Base articles.
|
|
9
|
+
|
|
10
|
+
Only ``get(id)`` is supported: the Megaplan API has NO endpoint to list
|
|
11
|
+
articles (``GET /api/v3/knowledgeArticle`` returns 404). To discover the
|
|
12
|
+
articles of a section, use ``client.knowledge_base.get_with_articles()``.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
async def get(self, article_id: int, use_cache: bool = True) -> KnowledgeArticle:
|
|
16
|
+
"""Get a Knowledge Base article by ID.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
article_id: Article identifier.
|
|
20
|
+
use_cache: Whether to use the entity cache (default: True).
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
The article, including its ``base`` (parent section) reference.
|
|
24
|
+
Note: ``article.parent`` is always None in the API — use ``base``.
|
|
25
|
+
"""
|
|
26
|
+
return await self._get_entity_cached(
|
|
27
|
+
"knowledgeArticle", article_id, KnowledgeArticle, use_cache=use_cache
|
|
28
|
+
)
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"""Knowledge Base section resource for Megaplan API."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
from collections.abc import AsyncIterator
|
|
7
|
+
from typing import TYPE_CHECKING, Any
|
|
8
|
+
|
|
9
|
+
from megaplan_sdk._knowledge_links import extract_article_ids
|
|
10
|
+
from megaplan_sdk.http_client import HTTPClient
|
|
11
|
+
from megaplan_sdk.logging_config import logger
|
|
12
|
+
from megaplan_sdk.models.knowledge import (
|
|
13
|
+
KnowledgeArticle,
|
|
14
|
+
KnowledgeBase,
|
|
15
|
+
KnowledgeSectionWithArticles,
|
|
16
|
+
)
|
|
17
|
+
from megaplan_sdk.resources.base import BaseResource
|
|
18
|
+
from megaplan_sdk.resources.knowledge_article import KnowledgeArticleResource
|
|
19
|
+
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
from megaplan_sdk.cache import EntityCache
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class KnowledgeBaseResource(BaseResource):
|
|
25
|
+
"""Resource for Knowledge Base sections.
|
|
26
|
+
|
|
27
|
+
Provides ``get`` / ``list`` / ``iterate`` over the working endpoints, plus
|
|
28
|
+
the experimental ``get_with_articles`` helper.
|
|
29
|
+
|
|
30
|
+
NOTE: the server-side ``parent`` filter on ``GET /api/v3/knowledgeBase`` is
|
|
31
|
+
ignored and sections are flat (no hierarchy), so ``list`` exposes no
|
|
32
|
+
``parent`` parameter. The real hierarchy is section -> articles via
|
|
33
|
+
``KnowledgeArticle.base``.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def __init__(
|
|
37
|
+
self,
|
|
38
|
+
http_client: HTTPClient,
|
|
39
|
+
cache: EntityCache | None = None,
|
|
40
|
+
article_resource: KnowledgeArticleResource | None = None,
|
|
41
|
+
) -> None:
|
|
42
|
+
super().__init__(http_client, cache=cache)
|
|
43
|
+
# Reused for get_with_articles; falls back to a self-built one (shared cache).
|
|
44
|
+
self._article_resource = article_resource or KnowledgeArticleResource(
|
|
45
|
+
http_client, cache=cache
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
async def get(self, section_id: int, use_cache: bool = True) -> KnowledgeBase:
|
|
49
|
+
"""Get a Knowledge Base section by ID (full object, including HTML content).
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
section_id: Section identifier.
|
|
53
|
+
use_cache: Whether to use the entity cache (default: True).
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
Full KnowledgeBase section including HTML content.
|
|
57
|
+
"""
|
|
58
|
+
return await self._get_entity_cached(
|
|
59
|
+
"knowledgeBase", section_id, KnowledgeBase, use_cache=use_cache
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
async def list(
|
|
63
|
+
self,
|
|
64
|
+
limit: int | None = None,
|
|
65
|
+
page_after: dict[str, Any] | None = None,
|
|
66
|
+
page_before: dict[str, Any] | None = None,
|
|
67
|
+
page_with: dict[str, Any] | None = None,
|
|
68
|
+
) -> list[KnowledgeBase]:
|
|
69
|
+
"""Get the flat list of Knowledge Base sections.
|
|
70
|
+
|
|
71
|
+
The API has no working ``parent`` filter and sections have no
|
|
72
|
+
hierarchy, so all sections are returned as a flat list.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
limit: Number of items per page.
|
|
76
|
+
page_after: Load page starting from this entity.
|
|
77
|
+
page_before: Load page strictly before this entity.
|
|
78
|
+
page_with: Load page containing this entity.
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
List of KnowledgeBase sections.
|
|
82
|
+
"""
|
|
83
|
+
path = self._build_path("api", "v3", "knowledgeBase")
|
|
84
|
+
params = self._build_list_params(
|
|
85
|
+
limit=limit,
|
|
86
|
+
page_after=page_after,
|
|
87
|
+
page_before=page_before,
|
|
88
|
+
page_with=page_with,
|
|
89
|
+
)
|
|
90
|
+
return await self._get_list(path, KnowledgeBase, params)
|
|
91
|
+
|
|
92
|
+
async def iterate(self, limit: int = 100, **kwargs: Any) -> AsyncIterator[KnowledgeBase]:
|
|
93
|
+
"""Iterate over all Knowledge Base sections with automatic pagination.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
limit: Number of items per page.
|
|
97
|
+
**kwargs: Additional parameters to pass to list().
|
|
98
|
+
|
|
99
|
+
Yields:
|
|
100
|
+
KnowledgeBase section objects.
|
|
101
|
+
"""
|
|
102
|
+
section: KnowledgeBase
|
|
103
|
+
async for section in self._iterate_generic( # type: ignore[valid-type]
|
|
104
|
+
"KnowledgeBase",
|
|
105
|
+
self.list,
|
|
106
|
+
limit,
|
|
107
|
+
**kwargs,
|
|
108
|
+
):
|
|
109
|
+
yield section
|
|
110
|
+
|
|
111
|
+
async def get_with_articles(
|
|
112
|
+
self, section_id: int, use_cache: bool = True
|
|
113
|
+
) -> KnowledgeSectionWithArticles:
|
|
114
|
+
"""Get a section together with its articles (EXPERIMENTAL).
|
|
115
|
+
|
|
116
|
+
The Megaplan API has no endpoint to list a section's articles, so this
|
|
117
|
+
discovers them by parsing ``/knowledge/<section>/<article>`` links in
|
|
118
|
+
the section's HTML ``content`` and fetching each article. Only articles
|
|
119
|
+
whose ``base.id`` equals ``section_id`` are kept (so a table-of-contents
|
|
120
|
+
section cannot pull in unrelated articles). Individual article failures
|
|
121
|
+
are skipped with a warning.
|
|
122
|
+
|
|
123
|
+
FRAGILE: depends on Megaplan's HTML link format. If the format changes,
|
|
124
|
+
``articles`` may come back empty even when the section has articles.
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
section_id: Section identifier.
|
|
128
|
+
use_cache: Whether to use the entity cache (default: True).
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
KnowledgeSectionWithArticles with section and its filtered articles.
|
|
132
|
+
"""
|
|
133
|
+
section = await self.get(section_id, use_cache=use_cache)
|
|
134
|
+
article_ids = extract_article_ids(section.content)
|
|
135
|
+
|
|
136
|
+
fetched = await asyncio.gather(
|
|
137
|
+
*(self._article_resource.get(aid, use_cache=use_cache) for aid in article_ids),
|
|
138
|
+
return_exceptions=True,
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
articles: list[KnowledgeArticle] = []
|
|
142
|
+
for aid, result in zip(article_ids, fetched, strict=True):
|
|
143
|
+
if isinstance(result, BaseException):
|
|
144
|
+
logger.warning("Failed to load knowledge article %s: %s", aid, result)
|
|
145
|
+
continue
|
|
146
|
+
if result.base is not None and result.base.id == section_id:
|
|
147
|
+
articles.append(result)
|
|
148
|
+
|
|
149
|
+
return KnowledgeSectionWithArticles(section=section, articles=articles)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: megaplan-sdk
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Professional Python SDK for Megaplan API v3
|
|
5
5
|
Author-email: Maxim Borzov <max@borzov.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -67,6 +67,7 @@ Dynamic: license-file
|
|
|
67
67
|
- [Задачи](#работа-с-задачами)
|
|
68
68
|
- [Проекты](#работа-с-проектами)
|
|
69
69
|
- [Сделки](#работа-со-сделками)
|
|
70
|
+
- [База знаний](#работа-с-базой-знаний)
|
|
70
71
|
|
|
71
72
|
### Продвинутые возможности
|
|
72
73
|
- [Кэширование сущностей](#кэширование-сущностей)
|
|
@@ -1123,6 +1124,45 @@ details = await client.deals.get_full_details(
|
|
|
1123
1124
|
|
|
1124
1125
|
> **Примечание:** Общее описание метода `get_full_details()` и примеры использования см. в разделе [Общие паттерны работы с сущностями](#общие-паттерны-работы-с-сущностями).
|
|
1125
1126
|
|
|
1127
|
+
## Работа с базой знаний
|
|
1128
|
+
|
|
1129
|
+
SDK предоставляет доступ к разделам и статьям Базы знаний Мегаплана через два ресурса: `client.knowledge_base` (разделы) и `client.knowledge_article` (статьи).
|
|
1130
|
+
|
|
1131
|
+
```python
|
|
1132
|
+
# Список разделов Базы знаний (плоский)
|
|
1133
|
+
sections = await client.knowledge_base.list()
|
|
1134
|
+
for s in sections:
|
|
1135
|
+
print(s.id, s.name)
|
|
1136
|
+
|
|
1137
|
+
# Один раздел с HTML-содержимым
|
|
1138
|
+
section = await client.knowledge_base.get(11)
|
|
1139
|
+
print(section.content)
|
|
1140
|
+
|
|
1141
|
+
# Итерация по всем разделам с автопагинацией
|
|
1142
|
+
async for section in client.knowledge_base.iterate():
|
|
1143
|
+
print(section.id, section.name)
|
|
1144
|
+
|
|
1145
|
+
# Статья по ID (parent всегда None — используйте base)
|
|
1146
|
+
article = await client.knowledge_article.get(33)
|
|
1147
|
+
print(article.name, "→ раздел:", article.base.name if article.base else None)
|
|
1148
|
+
|
|
1149
|
+
# Экспериментально: раздел вместе со статьями (через парсинг HTML-ссылок)
|
|
1150
|
+
bundle = await client.knowledge_base.get_with_articles(2)
|
|
1151
|
+
for a in bundle.articles:
|
|
1152
|
+
print(a.id, a.name)
|
|
1153
|
+
```
|
|
1154
|
+
|
|
1155
|
+
> **Известные ограничения API (серверная сторона):**
|
|
1156
|
+
>
|
|
1157
|
+
> - **Нет листинга статей:** эндпоинт `GET /api/v3/knowledgeArticle` отсутствует (возвращает 404).
|
|
1158
|
+
> Единственный способ обнаружения статей в разделе — метод `get_with_articles()`, который
|
|
1159
|
+
> парсит HTML-ссылки из поля `content` раздела. Это **экспериментальный** и хрупкий метод —
|
|
1160
|
+
> формат ссылок может измениться на стороне сервера.
|
|
1161
|
+
> - **Фильтр `parent` не работает:** `knowledge_base.list()` всегда возвращает плоский список
|
|
1162
|
+
> всех разделов; передача `parent` в фильтре игнорируется сервером. Иерархии разделов нет.
|
|
1163
|
+
> - **Поле `parent` у статьи всегда `null`:** для определения принадлежности статьи к разделу
|
|
1164
|
+
> используйте `article.base` (не `article.parent`).
|
|
1165
|
+
|
|
1126
1166
|
## Продвинутые возможности
|
|
1127
1167
|
|
|
1128
1168
|
### Кэширование сущностей
|
|
@@ -2,6 +2,7 @@ LICENSE
|
|
|
2
2
|
README.md
|
|
3
3
|
pyproject.toml
|
|
4
4
|
src/megaplan_sdk/__init__.py
|
|
5
|
+
src/megaplan_sdk/_knowledge_links.py
|
|
5
6
|
src/megaplan_sdk/auth.py
|
|
6
7
|
src/megaplan_sdk/cache.py
|
|
7
8
|
src/megaplan_sdk/client.py
|
|
@@ -27,6 +28,7 @@ src/megaplan_sdk/models/department.py
|
|
|
27
28
|
src/megaplan_sdk/models/employee.py
|
|
28
29
|
src/megaplan_sdk/models/filter.py
|
|
29
30
|
src/megaplan_sdk/models/group.py
|
|
31
|
+
src/megaplan_sdk/models/knowledge.py
|
|
30
32
|
src/megaplan_sdk/models/milestone.py
|
|
31
33
|
src/megaplan_sdk/models/participant.py
|
|
32
34
|
src/megaplan_sdk/models/project.py
|
|
@@ -41,5 +43,7 @@ src/megaplan_sdk/resources/departments.py
|
|
|
41
43
|
src/megaplan_sdk/resources/employees.py
|
|
42
44
|
src/megaplan_sdk/resources/filters.py
|
|
43
45
|
src/megaplan_sdk/resources/full_details.py
|
|
46
|
+
src/megaplan_sdk/resources/knowledge_article.py
|
|
47
|
+
src/megaplan_sdk/resources/knowledge_base.py
|
|
44
48
|
src/megaplan_sdk/resources/projects.py
|
|
45
49
|
src/megaplan_sdk/resources/tasks.py
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|