sgnl-api 0.0.1__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.
- sgnl_api-0.0.1/LICENSE +10 -0
- sgnl_api-0.0.1/PKG-INFO +82 -0
- sgnl_api-0.0.1/README.md +63 -0
- sgnl_api-0.0.1/setup.cfg +4 -0
- sgnl_api-0.0.1/setup.py +28 -0
- sgnl_api-0.0.1/sgnl_api/__init__.py +1 -0
- sgnl_api-0.0.1/sgnl_api/decorators.py +9 -0
- sgnl_api-0.0.1/sgnl_api/docs_api.py +501 -0
- sgnl_api-0.0.1/sgnl_api/file.py +18 -0
- sgnl_api-0.0.1/sgnl_api/scopes.py +19 -0
- sgnl_api-0.0.1/sgnl_api/utils.py +45 -0
- sgnl_api-0.0.1/sgnl_api.egg-info/PKG-INFO +82 -0
- sgnl_api-0.0.1/sgnl_api.egg-info/SOURCES.txt +14 -0
- sgnl_api-0.0.1/sgnl_api.egg-info/dependency_links.txt +1 -0
- sgnl_api-0.0.1/sgnl_api.egg-info/requires.txt +2 -0
- sgnl_api-0.0.1/sgnl_api.egg-info/top_level.txt +1 -0
sgnl_api-0.0.1/LICENSE
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) [2024] [Igor Gritsyuk]
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
6
|
+
copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
7
|
+
|
|
8
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
9
|
+
|
|
10
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
sgnl_api-0.0.1/PKG-INFO
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: sgnl-api
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Asynchronous python wrapper over API sgnl.pro
|
|
5
|
+
Home-page: https://github.com/gritsyuk/sgnl-api
|
|
6
|
+
Download-URL: https://github.com/gritsyuk/sgnl-api/archive/refs/heads/develop.zip
|
|
7
|
+
Author: Igor Gritsyuk
|
|
8
|
+
Author-email: gritsyuk.igor@gmail.com
|
|
9
|
+
Project-URL: GitHub, https://github.com/gritsyuk/sgnl-api
|
|
10
|
+
Keywords: sgnl api signal docs construction supervision operation inspections constarctionsite building management
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Requires-Python: >=3.9
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
License-File: LICENSE
|
|
17
|
+
Requires-Dist: httpx>=0.27.2
|
|
18
|
+
Requires-Dist: aiofiles>=24.1.0
|
|
19
|
+
|
|
20
|
+
# <img src="./img/logo.svg"> SIGNAL API
|
|
21
|
+
|
|
22
|
+
 [](https://t.me/prostobim)
|
|
23
|
+
## Обертка над API Signal
|
|
24
|
+
Официальная документация [https://api.sgnl.pro/openapi/swagger/index.html](https://api.sgnl.pro/openapi/swagger/index.html)
|
|
25
|
+
## Установка
|
|
26
|
+
```bash
|
|
27
|
+
pip install -U sgnl-api
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Пример
|
|
31
|
+
```python
|
|
32
|
+
import asyncio
|
|
33
|
+
import os
|
|
34
|
+
from sgnl_api import DocsApi
|
|
35
|
+
from dotenv import load_dotenv
|
|
36
|
+
|
|
37
|
+
load_dotenv()
|
|
38
|
+
CLIENT_ID = os.getenv("CLIENT_ID")
|
|
39
|
+
SECRET_ID = os.getenv("SECRET_ID")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
async def main():
|
|
43
|
+
|
|
44
|
+
docs = await DocsApi.create(
|
|
45
|
+
client_id=CLIENT_ID,
|
|
46
|
+
client_secret=SECRET_ID
|
|
47
|
+
)
|
|
48
|
+
projects = await docs.project.get_list()
|
|
49
|
+
for project in projects:
|
|
50
|
+
print(project)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
if __name__ == "__main__":
|
|
54
|
+
asyncio.run(main())
|
|
55
|
+
```
|
|
56
|
+
## Методы
|
|
57
|
+
| Методы | Описание |
|
|
58
|
+
|-----------------------------|-----------------------------------------|
|
|
59
|
+
| `Item.get_list` | Список файлов в директории |
|
|
60
|
+
| `Item.count` | Количество файлов в директории |
|
|
61
|
+
| `Item.download_link` | Получить ссылку на скачивание |
|
|
62
|
+
| `Item.create` | Создать файл |
|
|
63
|
+
| `Item.create_link` | Создать ссылку |
|
|
64
|
+
| `Item.new_version` | Создать новую версию |
|
|
65
|
+
| `Folder.get_list` | Список подкаталогов в каталоге |
|
|
66
|
+
| `Folder.create` | Создать каталог |
|
|
67
|
+
| `Folder.update` | Переименовать каталог |
|
|
68
|
+
| `Project.root_folder` | Данные корневой папки проекта |
|
|
69
|
+
| `Project.root_folder_id` | UUID корневой папки |
|
|
70
|
+
| `Project.get_list` | Список проектов |
|
|
71
|
+
| `Project.info` | Информация о проекте |
|
|
72
|
+
| `Project.users` | Список пользователей проекта |
|
|
73
|
+
| `Project.roles` | Список ролей проекта |
|
|
74
|
+
| `Project.users_permissions` | Права доступа у пользователя на проекте |
|
|
75
|
+
| `Company.users_list` | Список пользователей компании |
|
|
76
|
+
| `Company.roles_list` | Список ролей компании |
|
|
77
|
+
| `Version.list` | Список версий файла |
|
|
78
|
+
| `Version.count` | Количество версий у файла |
|
|
79
|
+
| `Version.new` | Новая версия |
|
|
80
|
+
| `File.get_object_upload` | Получение ссылки загрузки и id объекта |
|
|
81
|
+
| `File.commit_uploading` | Записать сведения в S3 |
|
|
82
|
+
| `File.upload` | Загрузка объекта полный цикл |
|
sgnl_api-0.0.1/README.md
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# <img src="./img/logo.svg"> SIGNAL API
|
|
2
|
+
|
|
3
|
+
 [](https://t.me/prostobim)
|
|
4
|
+
## Обертка над API Signal
|
|
5
|
+
Официальная документация [https://api.sgnl.pro/openapi/swagger/index.html](https://api.sgnl.pro/openapi/swagger/index.html)
|
|
6
|
+
## Установка
|
|
7
|
+
```bash
|
|
8
|
+
pip install -U sgnl-api
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Пример
|
|
12
|
+
```python
|
|
13
|
+
import asyncio
|
|
14
|
+
import os
|
|
15
|
+
from sgnl_api import DocsApi
|
|
16
|
+
from dotenv import load_dotenv
|
|
17
|
+
|
|
18
|
+
load_dotenv()
|
|
19
|
+
CLIENT_ID = os.getenv("CLIENT_ID")
|
|
20
|
+
SECRET_ID = os.getenv("SECRET_ID")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
async def main():
|
|
24
|
+
|
|
25
|
+
docs = await DocsApi.create(
|
|
26
|
+
client_id=CLIENT_ID,
|
|
27
|
+
client_secret=SECRET_ID
|
|
28
|
+
)
|
|
29
|
+
projects = await docs.project.get_list()
|
|
30
|
+
for project in projects:
|
|
31
|
+
print(project)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
if __name__ == "__main__":
|
|
35
|
+
asyncio.run(main())
|
|
36
|
+
```
|
|
37
|
+
## Методы
|
|
38
|
+
| Методы | Описание |
|
|
39
|
+
|-----------------------------|-----------------------------------------|
|
|
40
|
+
| `Item.get_list` | Список файлов в директории |
|
|
41
|
+
| `Item.count` | Количество файлов в директории |
|
|
42
|
+
| `Item.download_link` | Получить ссылку на скачивание |
|
|
43
|
+
| `Item.create` | Создать файл |
|
|
44
|
+
| `Item.create_link` | Создать ссылку |
|
|
45
|
+
| `Item.new_version` | Создать новую версию |
|
|
46
|
+
| `Folder.get_list` | Список подкаталогов в каталоге |
|
|
47
|
+
| `Folder.create` | Создать каталог |
|
|
48
|
+
| `Folder.update` | Переименовать каталог |
|
|
49
|
+
| `Project.root_folder` | Данные корневой папки проекта |
|
|
50
|
+
| `Project.root_folder_id` | UUID корневой папки |
|
|
51
|
+
| `Project.get_list` | Список проектов |
|
|
52
|
+
| `Project.info` | Информация о проекте |
|
|
53
|
+
| `Project.users` | Список пользователей проекта |
|
|
54
|
+
| `Project.roles` | Список ролей проекта |
|
|
55
|
+
| `Project.users_permissions` | Права доступа у пользователя на проекте |
|
|
56
|
+
| `Company.users_list` | Список пользователей компании |
|
|
57
|
+
| `Company.roles_list` | Список ролей компании |
|
|
58
|
+
| `Version.list` | Список версий файла |
|
|
59
|
+
| `Version.count` | Количество версий у файла |
|
|
60
|
+
| `Version.new` | Новая версия |
|
|
61
|
+
| `File.get_object_upload` | Получение ссылки загрузки и id объекта |
|
|
62
|
+
| `File.commit_uploading` | Записать сведения в S3 |
|
|
63
|
+
| `File.upload` | Загрузка объекта полный цикл |
|
sgnl_api-0.0.1/setup.cfg
ADDED
sgnl_api-0.0.1/setup.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
from setuptools import setup, find_packages
|
|
2
|
+
|
|
3
|
+
with open("README.md", "r", encoding="utf-8") as fh:
|
|
4
|
+
long_description = fh.read()
|
|
5
|
+
|
|
6
|
+
setup(
|
|
7
|
+
name='sgnl-api',
|
|
8
|
+
version='0.0.1',
|
|
9
|
+
author='Igor Gritsyuk',
|
|
10
|
+
author_email='gritsyuk.igor@gmail.com',
|
|
11
|
+
description='Asynchronous python wrapper over API sgnl.pro',
|
|
12
|
+
download_url='https://github.com/gritsyuk/sgnl-api/archive/refs/heads/develop.zip',
|
|
13
|
+
long_description=long_description,
|
|
14
|
+
long_description_content_type='text/markdown',
|
|
15
|
+
url='https://github.com/gritsyuk/sgnl-api',
|
|
16
|
+
packages=find_packages(include=['sgnl_api', 'sgnl_api.*']),
|
|
17
|
+
install_requires=['httpx>=0.27.2', 'aiofiles>=24.1.0'],
|
|
18
|
+
classifiers=[
|
|
19
|
+
'Programming Language :: Python :: 3',
|
|
20
|
+
'License :: OSI Approved :: MIT License',
|
|
21
|
+
'Operating System :: OS Independent'
|
|
22
|
+
],
|
|
23
|
+
keywords='sgnl api signal docs construction supervision operation inspections constarctionsite building management',
|
|
24
|
+
project_urls={
|
|
25
|
+
'GitHub': 'https://github.com/gritsyuk/sgnl-api'
|
|
26
|
+
},
|
|
27
|
+
python_requires='>=3.9'
|
|
28
|
+
)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .docs_api import DocsApi
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
def extract_key_from_response(key: str):
|
|
2
|
+
def decorator(func):
|
|
3
|
+
async def wrapper(*args, **kwargs):
|
|
4
|
+
response = await func(*args, **kwargs)
|
|
5
|
+
if isinstance(response, dict) and key in response:
|
|
6
|
+
return response[key]
|
|
7
|
+
return response
|
|
8
|
+
return wrapper
|
|
9
|
+
return decorator
|
|
@@ -0,0 +1,501 @@
|
|
|
1
|
+
import httpx
|
|
2
|
+
from uuid import UUID
|
|
3
|
+
from datetime import datetime, timedelta
|
|
4
|
+
from .scopes import SCOPES_DEFAULT
|
|
5
|
+
from .decorators import extract_key_from_response
|
|
6
|
+
from .file import read_file
|
|
7
|
+
from .utils import keys_to_snake_case
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class DocsApi:
|
|
11
|
+
"""DocsApi client for interacting with the SGNL DOCS API.
|
|
12
|
+
Do not initialize this class directly. Use the create() classmethod instead:
|
|
13
|
+
Example:
|
|
14
|
+
```python
|
|
15
|
+
api = await DocsApi.create(client_id="your_id", client_secret="your_secret")
|
|
16
|
+
```
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
def __init__(self,
|
|
20
|
+
*,
|
|
21
|
+
client_id: str,
|
|
22
|
+
client_secret: str,
|
|
23
|
+
scopes: list[str] = SCOPES_DEFAULT) -> None:
|
|
24
|
+
self.client_id = client_id
|
|
25
|
+
self.client_secret = client_secret
|
|
26
|
+
self.scopes = [*scopes]
|
|
27
|
+
self.base_url = "https://api.sgnl.pro/public/v1"
|
|
28
|
+
self._jwt = None
|
|
29
|
+
self._jwt_expires_at = None
|
|
30
|
+
|
|
31
|
+
self.item = self.Item(self)
|
|
32
|
+
self.folder = self.Folder(self)
|
|
33
|
+
self.project = self.Project(self)
|
|
34
|
+
self.company = self.Company(self)
|
|
35
|
+
self.version = self.Version(self)
|
|
36
|
+
self.file = self.File(self)
|
|
37
|
+
|
|
38
|
+
@classmethod
|
|
39
|
+
async def create(
|
|
40
|
+
cls,
|
|
41
|
+
*,
|
|
42
|
+
client_id: str,
|
|
43
|
+
client_secret: str,
|
|
44
|
+
scopes: list[str] = SCOPES_DEFAULT) -> "DocsApi":
|
|
45
|
+
"""Create and initialize a new DocsApi instance.
|
|
46
|
+
Args:
|
|
47
|
+
client_id: Your SGNL DOCS API client ID
|
|
48
|
+
client_secret: Your SGNL DOCS API client secret
|
|
49
|
+
scopes: List of scopes
|
|
50
|
+
Returns:
|
|
51
|
+
An initialized DocsApi instance
|
|
52
|
+
Example:
|
|
53
|
+
```python
|
|
54
|
+
api = await DocsApi.create(
|
|
55
|
+
client_id="your_id",
|
|
56
|
+
client_secret="your_secret"
|
|
57
|
+
)
|
|
58
|
+
```
|
|
59
|
+
"""
|
|
60
|
+
instance = cls(client_id=client_id, client_secret=client_secret, scopes=scopes)
|
|
61
|
+
await instance._ensure_token()
|
|
62
|
+
return instance
|
|
63
|
+
|
|
64
|
+
async def _ensure_token(self):
|
|
65
|
+
if self._jwt is None or (self._jwt_expires_at and datetime.now() >= self._jwt_expires_at):
|
|
66
|
+
try:
|
|
67
|
+
async with httpx.AsyncClient() as client:
|
|
68
|
+
response = await client.post(
|
|
69
|
+
f'{self.base_url}/auth/token',
|
|
70
|
+
json={
|
|
71
|
+
"clientId": self.client_id,
|
|
72
|
+
"clientSecret": self.client_secret,
|
|
73
|
+
"scopes": self.scopes
|
|
74
|
+
},
|
|
75
|
+
headers={'Content-Type': 'application/json'}
|
|
76
|
+
)
|
|
77
|
+
response.raise_for_status()
|
|
78
|
+
data = response.json()
|
|
79
|
+
self._jwt = data.get('token')
|
|
80
|
+
expires_in = data.get('expiresIn', 3600)
|
|
81
|
+
self._jwt_expires_at = datetime.now() + timedelta(seconds=expires_in)
|
|
82
|
+
except Exception as e:
|
|
83
|
+
raise Exception(f"AUTHENTICATION ERROR: {e}")
|
|
84
|
+
|
|
85
|
+
async def _make_request(
|
|
86
|
+
self,
|
|
87
|
+
method: str,
|
|
88
|
+
url: str,
|
|
89
|
+
params: dict = None,
|
|
90
|
+
json: dict = None,
|
|
91
|
+
headers: dict = None) -> str:
|
|
92
|
+
|
|
93
|
+
error_messages = {
|
|
94
|
+
400: "Bad request",
|
|
95
|
+
401: "The request did not have the correct authorization header credentials.",
|
|
96
|
+
403: "Forbidden",
|
|
97
|
+
404: "Not Found",
|
|
98
|
+
450: "Maximum resource quota reached. To increase the quota, contact support.",
|
|
99
|
+
500: "Api server error"
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
try:
|
|
103
|
+
await self._ensure_token()
|
|
104
|
+
async with httpx.AsyncClient() as client:
|
|
105
|
+
response = await client.request(
|
|
106
|
+
method=method,
|
|
107
|
+
url=url,
|
|
108
|
+
params=params,
|
|
109
|
+
json=json,
|
|
110
|
+
headers=headers)
|
|
111
|
+
|
|
112
|
+
if response.is_success:
|
|
113
|
+
return keys_to_snake_case(response.json())
|
|
114
|
+
else:
|
|
115
|
+
error_message = error_messages.get(response.status_code, "Неизвестная ошибка")
|
|
116
|
+
return {
|
|
117
|
+
"error": response.status_code,
|
|
118
|
+
"message": error_message
|
|
119
|
+
}
|
|
120
|
+
except Exception as e:
|
|
121
|
+
raise Exception(f"Error: {e}")
|
|
122
|
+
|
|
123
|
+
def _get_headers(self, additional_headers: dict = None) -> dict:
|
|
124
|
+
headers = {
|
|
125
|
+
"accept": "application/json",
|
|
126
|
+
"Authorization": f"Bearer {self._jwt}"
|
|
127
|
+
}
|
|
128
|
+
if additional_headers:
|
|
129
|
+
headers.update(additional_headers)
|
|
130
|
+
return headers
|
|
131
|
+
|
|
132
|
+
class Item:
|
|
133
|
+
def __init__(self, api: 'DocsApi'):
|
|
134
|
+
self.api = api
|
|
135
|
+
|
|
136
|
+
async def get_list(
|
|
137
|
+
self,
|
|
138
|
+
folder_id: UUID,
|
|
139
|
+
deleted: bool = False,
|
|
140
|
+
take: int = 100,
|
|
141
|
+
skip: int = 0
|
|
142
|
+
) -> list[dict]:
|
|
143
|
+
return await self.api._make_request(
|
|
144
|
+
method='GET',
|
|
145
|
+
url=f'{self.api.base_url}/items',
|
|
146
|
+
params={
|
|
147
|
+
'folderId': folder_id,
|
|
148
|
+
'take': take,
|
|
149
|
+
'skip': skip,
|
|
150
|
+
'deleted': deleted
|
|
151
|
+
},
|
|
152
|
+
headers=self.api._get_headers()
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
@extract_key_from_response(key="data")
|
|
156
|
+
async def count(
|
|
157
|
+
self,
|
|
158
|
+
folder_id: UUID,
|
|
159
|
+
deleted: bool = False
|
|
160
|
+
) -> int:
|
|
161
|
+
return await self.api._make_request(
|
|
162
|
+
method='GET',
|
|
163
|
+
url=f'{self.api.base_url}/items/count',
|
|
164
|
+
params={
|
|
165
|
+
'folderId': folder_id,
|
|
166
|
+
'deleted': deleted
|
|
167
|
+
},
|
|
168
|
+
headers=self.api._get_headers()
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
async def download_link(
|
|
172
|
+
self,
|
|
173
|
+
folder_id: UUID,
|
|
174
|
+
version_id: UUID,
|
|
175
|
+
file_name: str = None
|
|
176
|
+
):
|
|
177
|
+
params = {
|
|
178
|
+
'folderId': folder_id,
|
|
179
|
+
'versionId': version_id
|
|
180
|
+
}
|
|
181
|
+
if file_name:
|
|
182
|
+
params['fileName'] = file_name
|
|
183
|
+
return await self.api._make_request(
|
|
184
|
+
method='GET',
|
|
185
|
+
url=f'{self.api.base_url}/items/download',
|
|
186
|
+
params=params,
|
|
187
|
+
headers=self.api._get_headers()
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
async def create(
|
|
191
|
+
self,
|
|
192
|
+
name: str,
|
|
193
|
+
folder_id: UUID,
|
|
194
|
+
version_id: UUID
|
|
195
|
+
) -> dict:
|
|
196
|
+
return await self.api._make_request(
|
|
197
|
+
method='PUT',
|
|
198
|
+
url=f'{self.api.base_url}/items/file',
|
|
199
|
+
json={
|
|
200
|
+
'name': name,
|
|
201
|
+
'folderId': folder_id,
|
|
202
|
+
'versionId': version_id
|
|
203
|
+
},
|
|
204
|
+
headers=self.api._get_headers()
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
async def create_link(
|
|
208
|
+
self,
|
|
209
|
+
name: str,
|
|
210
|
+
folder_id: UUID,
|
|
211
|
+
version_id: UUID
|
|
212
|
+
) -> dict:
|
|
213
|
+
return await self.api._make_request(
|
|
214
|
+
method='PUT',
|
|
215
|
+
url=f'{self.api.base_url}/items/link',
|
|
216
|
+
json={
|
|
217
|
+
'name': name,
|
|
218
|
+
'folderId': folder_id,
|
|
219
|
+
'versionId': version_id
|
|
220
|
+
},
|
|
221
|
+
headers=self.api._get_headers()
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
async def new_version(
|
|
225
|
+
self,
|
|
226
|
+
item_id: UUID,
|
|
227
|
+
version_id: UUID
|
|
228
|
+
) -> dict:
|
|
229
|
+
return await self.api._make_request(
|
|
230
|
+
method='PUT',
|
|
231
|
+
url=f'{self.api.base_url}/items/versions',
|
|
232
|
+
json={
|
|
233
|
+
'folderId': item_id,
|
|
234
|
+
'versionId': version_id
|
|
235
|
+
},
|
|
236
|
+
headers=self.api._get_headers()
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
class Folder:
|
|
240
|
+
def __init__(self, api: 'DocsApi'):
|
|
241
|
+
self.api = api
|
|
242
|
+
|
|
243
|
+
async def get_list(
|
|
244
|
+
self,
|
|
245
|
+
folder_id: UUID,
|
|
246
|
+
deleted: bool = False
|
|
247
|
+
) -> list[dict]:
|
|
248
|
+
return await self.api._make_request(
|
|
249
|
+
method='GET',
|
|
250
|
+
url=f'{self.api.base_url}/folders/{folder_id}/children',
|
|
251
|
+
params={
|
|
252
|
+
'deleted': deleted
|
|
253
|
+
},
|
|
254
|
+
headers=self.api._get_headers()
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
async def create(
|
|
258
|
+
self,
|
|
259
|
+
parent_id: UUID,
|
|
260
|
+
name: str = 'new folder from api'
|
|
261
|
+
) -> dict:
|
|
262
|
+
return await self.api._make_request(
|
|
263
|
+
method='PUT',
|
|
264
|
+
url=f'{self.api.base_url}/folders',
|
|
265
|
+
json={
|
|
266
|
+
'parentId': parent_id,
|
|
267
|
+
'name': name
|
|
268
|
+
},
|
|
269
|
+
headers=self.api._get_headers()
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
async def update(
|
|
273
|
+
self,
|
|
274
|
+
folder_id: UUID,
|
|
275
|
+
name: str
|
|
276
|
+
) -> dict:
|
|
277
|
+
return await self.api._make_request(
|
|
278
|
+
method='PATCH',
|
|
279
|
+
url=f'{self.api.base_url}/folders',
|
|
280
|
+
json={
|
|
281
|
+
'id': folder_id,
|
|
282
|
+
'name': name
|
|
283
|
+
},
|
|
284
|
+
headers=self.api._get_headers()
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
class Project:
|
|
288
|
+
def __init__(self, api: 'DocsApi'):
|
|
289
|
+
self.api = api
|
|
290
|
+
|
|
291
|
+
async def root_folder(
|
|
292
|
+
self,
|
|
293
|
+
project_id: UUID) -> dict:
|
|
294
|
+
return await self.api._make_request(
|
|
295
|
+
method='GET',
|
|
296
|
+
url=f'{self.api.base_url}/docs/projects/{project_id}',
|
|
297
|
+
headers=self.api._get_headers()
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
@extract_key_from_response(key='root_folder_id')
|
|
301
|
+
async def root_folder_id(self, project_id: UUID) -> dict:
|
|
302
|
+
return await self.root_folder(project_id)
|
|
303
|
+
|
|
304
|
+
async def get_list(
|
|
305
|
+
self,
|
|
306
|
+
take: int = 100,
|
|
307
|
+
skip: int = 0
|
|
308
|
+
) -> list[dict]:
|
|
309
|
+
return await self.api._make_request(
|
|
310
|
+
method='GET',
|
|
311
|
+
url=f'{self.api.base_url}/projects',
|
|
312
|
+
params={
|
|
313
|
+
'Take': take,
|
|
314
|
+
'Skip': skip
|
|
315
|
+
},
|
|
316
|
+
headers=self.api._get_headers()
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
async def info(
|
|
320
|
+
self,
|
|
321
|
+
project_id: UUID
|
|
322
|
+
) -> dict:
|
|
323
|
+
return await self.api._make_request(
|
|
324
|
+
method='GET',
|
|
325
|
+
url=f'{self.api.base_url}/projects/{project_id}',
|
|
326
|
+
headers=self.api._get_headers()
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
async def users(
|
|
330
|
+
self,
|
|
331
|
+
project_id: UUID,
|
|
332
|
+
take: int = 100,
|
|
333
|
+
skip: int = 0
|
|
334
|
+
) -> list[dict]:
|
|
335
|
+
return await self.api._make_request(
|
|
336
|
+
method='GET',
|
|
337
|
+
url=f'{self.api.base_url}/projects/{project_id}/users',
|
|
338
|
+
params={
|
|
339
|
+
'Take': take,
|
|
340
|
+
'Skip': skip
|
|
341
|
+
},
|
|
342
|
+
headers=self.api._get_headers()
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
async def roles(
|
|
346
|
+
self,
|
|
347
|
+
project_id: UUID,
|
|
348
|
+
take: int = 100,
|
|
349
|
+
skip: int = 0
|
|
350
|
+
) -> list[dict]:
|
|
351
|
+
return await self.api._make_request(
|
|
352
|
+
method='GET',
|
|
353
|
+
url=f'{self.api.base_url}/projects/{project_id}/roles',
|
|
354
|
+
params={
|
|
355
|
+
'Take': take,
|
|
356
|
+
'Skip': skip
|
|
357
|
+
},
|
|
358
|
+
headers=self.api._get_headers()
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
async def users_permissions(
|
|
362
|
+
self,
|
|
363
|
+
project_id: UUID,
|
|
364
|
+
user_id: UUID
|
|
365
|
+
) -> dict:
|
|
366
|
+
return await self.api._make_request(
|
|
367
|
+
method='GET',
|
|
368
|
+
url=f'{self.api.base_url}/projects/{project_id}/users/{user_id}/permission',
|
|
369
|
+
headers=self.api._get_headers()
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
class Company:
|
|
373
|
+
def __init__(self, api: 'DocsApi'):
|
|
374
|
+
self.api = api
|
|
375
|
+
|
|
376
|
+
async def users_list(self, take: int = 100, skip: int = 0) -> list[dict]:
|
|
377
|
+
return await self.api._make_request(
|
|
378
|
+
method='GET',
|
|
379
|
+
url=f'{self.api.base_url}/company/users',
|
|
380
|
+
params={
|
|
381
|
+
'Take': take,
|
|
382
|
+
'Skip': skip
|
|
383
|
+
},
|
|
384
|
+
headers=self.api._get_headers()
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
async def roles_list(self, take: int = 100, skip: int = 0) -> list[dict]:
|
|
388
|
+
return await self.api._make_request(
|
|
389
|
+
method='GET',
|
|
390
|
+
url=f'{self.api.base_url}/company/roles',
|
|
391
|
+
params={
|
|
392
|
+
'Take': take,
|
|
393
|
+
'Skip': skip
|
|
394
|
+
},
|
|
395
|
+
headers=self.api._get_headers()
|
|
396
|
+
)
|
|
397
|
+
|
|
398
|
+
class Version:
|
|
399
|
+
def __init__(self, api: 'DocsApi'):
|
|
400
|
+
self.api = api
|
|
401
|
+
|
|
402
|
+
async def list(
|
|
403
|
+
self,
|
|
404
|
+
item_id: UUID,
|
|
405
|
+
take: int = 100,
|
|
406
|
+
skip: int = 0
|
|
407
|
+
) -> list[dict]:
|
|
408
|
+
return await self.api._make_request(
|
|
409
|
+
method='GET',
|
|
410
|
+
url=f'{self.api.base_url}/versions',
|
|
411
|
+
params={
|
|
412
|
+
'itemId': item_id,
|
|
413
|
+
'Take': take,
|
|
414
|
+
'Skip': skip
|
|
415
|
+
},
|
|
416
|
+
headers=self.api._get_headers()
|
|
417
|
+
)
|
|
418
|
+
|
|
419
|
+
@extract_key_from_response(key="data")
|
|
420
|
+
async def count(
|
|
421
|
+
self,
|
|
422
|
+
item_id: UUID
|
|
423
|
+
) -> dict:
|
|
424
|
+
return await self.api._make_request(
|
|
425
|
+
method='GET',
|
|
426
|
+
url=f'{self.api.base_url}/versions',
|
|
427
|
+
params={
|
|
428
|
+
'itemId': item_id
|
|
429
|
+
},
|
|
430
|
+
headers=self.api._get_headers()
|
|
431
|
+
)
|
|
432
|
+
|
|
433
|
+
async def new(
|
|
434
|
+
self,
|
|
435
|
+
*,
|
|
436
|
+
object_id: UUID,
|
|
437
|
+
project_id: UUID,
|
|
438
|
+
name: str
|
|
439
|
+
) -> dict:
|
|
440
|
+
return await self.api._make_request(
|
|
441
|
+
method='POST',
|
|
442
|
+
url=f'{self.api.base_url}/versions',
|
|
443
|
+
json={
|
|
444
|
+
'objectId': object_id,
|
|
445
|
+
'projectId': project_id,
|
|
446
|
+
'name': name
|
|
447
|
+
},
|
|
448
|
+
headers=self.api._get_headers()
|
|
449
|
+
)
|
|
450
|
+
|
|
451
|
+
class File:
|
|
452
|
+
def __init__(self, api: 'DocsApi'):
|
|
453
|
+
self.api = api
|
|
454
|
+
|
|
455
|
+
async def get_object_upload(
|
|
456
|
+
self,
|
|
457
|
+
project_id: UUID,
|
|
458
|
+
mime_type: str,
|
|
459
|
+
size: int
|
|
460
|
+
) -> dict:
|
|
461
|
+
return await self.api._make_request(
|
|
462
|
+
method='PUT',
|
|
463
|
+
url=f'{self.api.base_url}/objects',
|
|
464
|
+
json={
|
|
465
|
+
"projectId": project_id,
|
|
466
|
+
"mimeType": mime_type,
|
|
467
|
+
"size": size
|
|
468
|
+
},
|
|
469
|
+
headers=self.api._get_headers()
|
|
470
|
+
)
|
|
471
|
+
|
|
472
|
+
async def commit_uploading(
|
|
473
|
+
self,
|
|
474
|
+
object_id: UUID
|
|
475
|
+
) -> dict:
|
|
476
|
+
return await self.api._make_request(
|
|
477
|
+
method='POST',
|
|
478
|
+
url=f'{self.api.base_url}/objects/{object_id}/uploading/commit',
|
|
479
|
+
headers=self.api._get_headers()
|
|
480
|
+
)
|
|
481
|
+
|
|
482
|
+
async def upload(self, project_id: UUID, file_path: str):
|
|
483
|
+
file_info = await read_file(file_path)
|
|
484
|
+
if file_info is None:
|
|
485
|
+
raise FileExistsError
|
|
486
|
+
file_data, mime_type, size = file_info
|
|
487
|
+
object_upload = await self.get_object_upload(project_id, mime_type, size)
|
|
488
|
+
async with httpx.AsyncClient() as client:
|
|
489
|
+
upload_response = await client.put(
|
|
490
|
+
object_upload.get("signed_url"),
|
|
491
|
+
content=file_data
|
|
492
|
+
)
|
|
493
|
+
if upload_response.status_code != 200:
|
|
494
|
+
return {
|
|
495
|
+
"error": upload_response.status_code,
|
|
496
|
+
"message": upload_response.text
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
result_status = await self.commit_uploading(project_id, object_upload.get("object_id"))
|
|
500
|
+
|
|
501
|
+
return result_status
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import aiofiles
|
|
2
|
+
import mimetypes
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
async def read_file(file_path):
|
|
7
|
+
async with aiofiles.open(file_path, 'rb') as file:
|
|
8
|
+
file_data = await file.read()
|
|
9
|
+
|
|
10
|
+
mime_type, _ = mimetypes.guess_type(file_path)
|
|
11
|
+
size = os.path.getsize(file_path)
|
|
12
|
+
|
|
13
|
+
if size == 0 or mime_type is None:
|
|
14
|
+
return None
|
|
15
|
+
|
|
16
|
+
return file_data, mime_type, size
|
|
17
|
+
|
|
18
|
+
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
SCOPES_DEFAULT = (
|
|
2
|
+
"project:read",
|
|
3
|
+
"project:list",
|
|
4
|
+
"project:user:list",
|
|
5
|
+
"project:user:read",
|
|
6
|
+
"project:role:list",
|
|
7
|
+
"docs:object:create",
|
|
8
|
+
"docs:signing:list",
|
|
9
|
+
"docs:signing:create",
|
|
10
|
+
"docs:folder:list",
|
|
11
|
+
"docs:folder:create",
|
|
12
|
+
"docs:folder:update",
|
|
13
|
+
"docs:item:create",
|
|
14
|
+
"docs:item:list",
|
|
15
|
+
"docs:item:read",
|
|
16
|
+
"docs:version:create",
|
|
17
|
+
"docs:version:create",
|
|
18
|
+
"docs:version:list"
|
|
19
|
+
)
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from typing import Dict, List, Any, TypeVar, Optional
|
|
3
|
+
from functools import lru_cache
|
|
4
|
+
|
|
5
|
+
T = TypeVar('T', Dict, List, Any)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@lru_cache(maxsize=128)
|
|
9
|
+
def camel_to_snake(string: str) -> str:
|
|
10
|
+
"""
|
|
11
|
+
Конвертирует строку из camelCase в snake_case.
|
|
12
|
+
Args:
|
|
13
|
+
string: Строка в формате camelCase
|
|
14
|
+
Returns:
|
|
15
|
+
Строка в формате snake_case
|
|
16
|
+
Examples:
|
|
17
|
+
>>> camel_to_snake("camelCase")
|
|
18
|
+
'camel_case'
|
|
19
|
+
>>> camel_to_snake("HTTPResponse")
|
|
20
|
+
'http_response'
|
|
21
|
+
"""
|
|
22
|
+
string = re.sub(r'([A-Z]+)([A-Z][a-z])', r'\1_\2', string)
|
|
23
|
+
string = re.sub(r'([a-z0-9])([A-Z])', r'\1_\2', string)
|
|
24
|
+
return string.lower()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def keys_to_snake_case(data: T, max_depth: Optional[int] = None, _current_depth: int = 0) -> T:
|
|
28
|
+
if max_depth is not None and _current_depth > max_depth:
|
|
29
|
+
raise ValueError(f"Maximum depth exceeded: {max_depth}")
|
|
30
|
+
|
|
31
|
+
if isinstance(data, dict):
|
|
32
|
+
return {
|
|
33
|
+
camel_to_snake(str(key)): keys_to_snake_case(value, max_depth, _current_depth + 1)
|
|
34
|
+
if isinstance(value, (dict, list))
|
|
35
|
+
else value
|
|
36
|
+
for key, value in data.items()
|
|
37
|
+
}
|
|
38
|
+
elif isinstance(data, list):
|
|
39
|
+
return [
|
|
40
|
+
keys_to_snake_case(item, max_depth, _current_depth + 1)
|
|
41
|
+
if isinstance(item, (dict, list))
|
|
42
|
+
else item
|
|
43
|
+
for item in data
|
|
44
|
+
]
|
|
45
|
+
return data
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: sgnl-api
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Asynchronous python wrapper over API sgnl.pro
|
|
5
|
+
Home-page: https://github.com/gritsyuk/sgnl-api
|
|
6
|
+
Download-URL: https://github.com/gritsyuk/sgnl-api/archive/refs/heads/develop.zip
|
|
7
|
+
Author: Igor Gritsyuk
|
|
8
|
+
Author-email: gritsyuk.igor@gmail.com
|
|
9
|
+
Project-URL: GitHub, https://github.com/gritsyuk/sgnl-api
|
|
10
|
+
Keywords: sgnl api signal docs construction supervision operation inspections constarctionsite building management
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Requires-Python: >=3.9
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
License-File: LICENSE
|
|
17
|
+
Requires-Dist: httpx>=0.27.2
|
|
18
|
+
Requires-Dist: aiofiles>=24.1.0
|
|
19
|
+
|
|
20
|
+
# <img src="./img/logo.svg"> SIGNAL API
|
|
21
|
+
|
|
22
|
+
 [](https://t.me/prostobim)
|
|
23
|
+
## Обертка над API Signal
|
|
24
|
+
Официальная документация [https://api.sgnl.pro/openapi/swagger/index.html](https://api.sgnl.pro/openapi/swagger/index.html)
|
|
25
|
+
## Установка
|
|
26
|
+
```bash
|
|
27
|
+
pip install -U sgnl-api
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Пример
|
|
31
|
+
```python
|
|
32
|
+
import asyncio
|
|
33
|
+
import os
|
|
34
|
+
from sgnl_api import DocsApi
|
|
35
|
+
from dotenv import load_dotenv
|
|
36
|
+
|
|
37
|
+
load_dotenv()
|
|
38
|
+
CLIENT_ID = os.getenv("CLIENT_ID")
|
|
39
|
+
SECRET_ID = os.getenv("SECRET_ID")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
async def main():
|
|
43
|
+
|
|
44
|
+
docs = await DocsApi.create(
|
|
45
|
+
client_id=CLIENT_ID,
|
|
46
|
+
client_secret=SECRET_ID
|
|
47
|
+
)
|
|
48
|
+
projects = await docs.project.get_list()
|
|
49
|
+
for project in projects:
|
|
50
|
+
print(project)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
if __name__ == "__main__":
|
|
54
|
+
asyncio.run(main())
|
|
55
|
+
```
|
|
56
|
+
## Методы
|
|
57
|
+
| Методы | Описание |
|
|
58
|
+
|-----------------------------|-----------------------------------------|
|
|
59
|
+
| `Item.get_list` | Список файлов в директории |
|
|
60
|
+
| `Item.count` | Количество файлов в директории |
|
|
61
|
+
| `Item.download_link` | Получить ссылку на скачивание |
|
|
62
|
+
| `Item.create` | Создать файл |
|
|
63
|
+
| `Item.create_link` | Создать ссылку |
|
|
64
|
+
| `Item.new_version` | Создать новую версию |
|
|
65
|
+
| `Folder.get_list` | Список подкаталогов в каталоге |
|
|
66
|
+
| `Folder.create` | Создать каталог |
|
|
67
|
+
| `Folder.update` | Переименовать каталог |
|
|
68
|
+
| `Project.root_folder` | Данные корневой папки проекта |
|
|
69
|
+
| `Project.root_folder_id` | UUID корневой папки |
|
|
70
|
+
| `Project.get_list` | Список проектов |
|
|
71
|
+
| `Project.info` | Информация о проекте |
|
|
72
|
+
| `Project.users` | Список пользователей проекта |
|
|
73
|
+
| `Project.roles` | Список ролей проекта |
|
|
74
|
+
| `Project.users_permissions` | Права доступа у пользователя на проекте |
|
|
75
|
+
| `Company.users_list` | Список пользователей компании |
|
|
76
|
+
| `Company.roles_list` | Список ролей компании |
|
|
77
|
+
| `Version.list` | Список версий файла |
|
|
78
|
+
| `Version.count` | Количество версий у файла |
|
|
79
|
+
| `Version.new` | Новая версия |
|
|
80
|
+
| `File.get_object_upload` | Получение ссылки загрузки и id объекта |
|
|
81
|
+
| `File.commit_uploading` | Записать сведения в S3 |
|
|
82
|
+
| `File.upload` | Загрузка объекта полный цикл |
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
setup.py
|
|
4
|
+
sgnl_api/__init__.py
|
|
5
|
+
sgnl_api/decorators.py
|
|
6
|
+
sgnl_api/docs_api.py
|
|
7
|
+
sgnl_api/file.py
|
|
8
|
+
sgnl_api/scopes.py
|
|
9
|
+
sgnl_api/utils.py
|
|
10
|
+
sgnl_api.egg-info/PKG-INFO
|
|
11
|
+
sgnl_api.egg-info/SOURCES.txt
|
|
12
|
+
sgnl_api.egg-info/dependency_links.txt
|
|
13
|
+
sgnl_api.egg-info/requires.txt
|
|
14
|
+
sgnl_api.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
sgnl_api
|