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 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.
@@ -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
+ ![PyPI - Version](https://img.shields.io/pypi/v/sgnl-api) [![Telegram chat](https://img.shields.io/badge/Просто_о_BIM-join-blue?logo=telegram)](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,63 @@
1
+ # <img src="./img/logo.svg"> SIGNAL API
2
+
3
+ ![PyPI - Version](https://img.shields.io/pypi/v/sgnl-api) [![Telegram chat](https://img.shields.io/badge/Просто_о_BIM-join-blue?logo=telegram)](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` | Загрузка объекта полный цикл |
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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
+ ![PyPI - Version](https://img.shields.io/pypi/v/sgnl-api) [![Telegram chat](https://img.shields.io/badge/Просто_о_BIM-join-blue?logo=telegram)](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,2 @@
1
+ httpx>=0.27.2
2
+ aiofiles>=24.1.0
@@ -0,0 +1 @@
1
+ sgnl_api