assistagro-client 0.3.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.
Files changed (40) hide show
  1. assistagro_client-0.3.1/PKG-INFO +171 -0
  2. assistagro_client-0.3.1/README.md +155 -0
  3. assistagro_client-0.3.1/pyproject.toml +39 -0
  4. assistagro_client-0.3.1/setup.cfg +4 -0
  5. assistagro_client-0.3.1/src/assistagro_client/__init__.py +20 -0
  6. assistagro_client-0.3.1/src/assistagro_client/api/__init__.py +1 -0
  7. assistagro_client-0.3.1/src/assistagro_client/api/v1/__init__.py +1 -0
  8. assistagro_client-0.3.1/src/assistagro_client/api/v1/accounts.py +42 -0
  9. assistagro_client-0.3.1/src/assistagro_client/api/v1/companies.py +108 -0
  10. assistagro_client-0.3.1/src/assistagro_client/api/v1/dictionaries.py +691 -0
  11. assistagro_client-0.3.1/src/assistagro_client/api/v1/fields.py +147 -0
  12. assistagro_client-0.3.1/src/assistagro_client/api/v1/meteostations.py +152 -0
  13. assistagro_client-0.3.1/src/assistagro_client/api/v1/reports.py +157 -0
  14. assistagro_client-0.3.1/src/assistagro_client/api/v1/seasons.py +99 -0
  15. assistagro_client-0.3.1/src/assistagro_client/api/v1/structures.py +53 -0
  16. assistagro_client-0.3.1/src/assistagro_client/api/v1/tasks.py +135 -0
  17. assistagro_client-0.3.1/src/assistagro_client/api/v1/techmaps.py +71 -0
  18. assistagro_client-0.3.1/src/assistagro_client/auth.py +114 -0
  19. assistagro_client-0.3.1/src/assistagro_client/client.py +500 -0
  20. assistagro_client-0.3.1/src/assistagro_client/config.py +15 -0
  21. assistagro_client-0.3.1/src/assistagro_client/exceptions.py +27 -0
  22. assistagro_client-0.3.1/src/assistagro_client/models/__init__.py +56 -0
  23. assistagro_client-0.3.1/src/assistagro_client/models/accounts.py +21 -0
  24. assistagro_client-0.3.1/src/assistagro_client/models/companies.py +45 -0
  25. assistagro_client-0.3.1/src/assistagro_client/models/dictionaries.py +38 -0
  26. assistagro_client-0.3.1/src/assistagro_client/models/fields.py +33 -0
  27. assistagro_client-0.3.1/src/assistagro_client/models/meteostations.py +77 -0
  28. assistagro_client-0.3.1/src/assistagro_client/models/reports.py +29 -0
  29. assistagro_client-0.3.1/src/assistagro_client/models/seasons.py +33 -0
  30. assistagro_client-0.3.1/src/assistagro_client/models/structures.py +21 -0
  31. assistagro_client-0.3.1/src/assistagro_client/models/tasks.py +30 -0
  32. assistagro_client-0.3.1/src/assistagro_client/models/techmaps.py +38 -0
  33. assistagro_client-0.3.1/src/assistagro_client/py.typed +0 -0
  34. assistagro_client-0.3.1/src/assistagro_client.egg-info/PKG-INFO +171 -0
  35. assistagro_client-0.3.1/src/assistagro_client.egg-info/SOURCES.txt +38 -0
  36. assistagro_client-0.3.1/src/assistagro_client.egg-info/dependency_links.txt +1 -0
  37. assistagro_client-0.3.1/src/assistagro_client.egg-info/requires.txt +5 -0
  38. assistagro_client-0.3.1/src/assistagro_client.egg-info/top_level.txt +1 -0
  39. assistagro_client-0.3.1/tests/test_auth.py +154 -0
  40. assistagro_client-0.3.1/tests/test_client.py +229 -0
@@ -0,0 +1,171 @@
1
+ Metadata-Version: 2.4
2
+ Name: assistagro-client
3
+ Version: 0.3.1
4
+ Summary: HTTP client for AssistAgro API
5
+ Author-email: Dmitriy Kazakov <dmitriyfile@yandex.ru>
6
+ License-Expression: MIT
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: Programming Language :: Python :: 3.13
9
+ Requires-Python: >=3.10
10
+ Description-Content-Type: text/markdown
11
+ Requires-Dist: httpx>=0.28.1
12
+ Requires-Dist: pydantic>=2.12.5
13
+ Requires-Dist: pydantic-settings>=2.13.1
14
+ Requires-Dist: python-dateutil>=2.9.0.post0
15
+ Requires-Dist: python-dotenv>=1.0.0
16
+
17
+ # AssistAgro API Client
18
+
19
+ Синхронный и асинхронный HTTP-клиент для AssistAgro API.
20
+
21
+ ## Установка
22
+
23
+ ```bash
24
+ pip install assistagro-client
25
+ ```
26
+
27
+ ## Асинхронное использование
28
+
29
+ ```python
30
+ import asyncio
31
+ from assistagro_client import AssistAgroClient
32
+
33
+ async def main():
34
+ async with AssistAgroClient(base_url="https://gateway-frontend.agroassist.ru") as client:
35
+ tokens = await client.auth.sign_in(
36
+ email="user@example.com",
37
+ password="password123"
38
+ )
39
+ print(f"Access token: {tokens.access_token[:20]}...")
40
+
41
+ structures = await client.structures.list_()
42
+ structure_guid = structures[0].structure_guid
43
+
44
+ season = await client.seasons.get_current()
45
+
46
+ fields = await client.fields.list_(
47
+ structure_guids=[structure_guid],
48
+ season_id=season.season_id,
49
+ )
50
+ print(f"Найдено полей: {len(fields)}")
51
+
52
+ crops_response = await client.dictionaries.get_crops()
53
+ print(f"Культур: {len(crops_response.crops)}")
54
+
55
+ tasks = await client.tasks.list_(structure_guid=structure_guid)
56
+ print(f"Найдено задач: {len(tasks)}")
57
+
58
+ if __name__ == "__main__":
59
+ asyncio.run(main())
60
+ ```
61
+
62
+ ## Синхронное использование
63
+
64
+ ```python
65
+ from assistagro_client import AssistAgroSyncClient
66
+
67
+ with AssistAgroSyncClient(base_url="https://gateway-frontend.agroassist.ru") as client:
68
+ tokens = client.auth.sign_in(
69
+ email="user@example.com",
70
+ password="password123",
71
+ )
72
+ print(f"Access token: {tokens.access_token[:20]}...")
73
+
74
+ structures = client.structures.list_()
75
+ print(f"Найдено структур: {len(structures)}")
76
+ ```
77
+
78
+ `AssistAgroClient` остается асинхронным клиентом для обратной совместимости.
79
+ `AssistAgroSyncClient` предоставляет тот же набор API-разделов для обычного синхронного кода.
80
+ Если нужно передать контекст пользователя в `x-user-data`, используйте `set_user_data(...)`;
81
+ заголовок сериализуется как компактный JSON, например
82
+ `{"company_guid":"...","user_guid":"..."}`.
83
+
84
+ ## TLS/SSL сертификаты
85
+
86
+ По умолчанию клиент проверяет TLS-сертификат сервера. Если окружение использует свой
87
+ CA-bundle, передайте путь к нему:
88
+
89
+ ```python
90
+ client = AssistAgroClient(verify="/path/to/ca-bundle.pem")
91
+ ```
92
+
93
+ Если сервер временно отдает некорректную цепочку сертификатов, проверку можно отключить
94
+ явно:
95
+
96
+ ```python
97
+ client = AssistAgroClient(verify=False)
98
+ ```
99
+
100
+ То же доступно для `AssistAgroSyncClient` и через переменную окружения
101
+ `ASSISTAGRO_VERIFY_SSL=false`.
102
+
103
+ ## API эндпоинты
104
+
105
+ | Модуль | Описание | Основные методы |
106
+ |--------|----------|-----------------|
107
+ | **auth** | Аутентификация | `sign_in`, `refresh_tokens`, `logout` |
108
+ | **accounts** | Пользователи | `get_current_user`, `get_users` |
109
+ | **companies** | Компании | `list_()`, `list_all`, `get`, `get_license`, `get_license_metadata`, `get_activities`, `get_dashboards` |
110
+ | **structures** | Структуры | `list_()`, `list_plain` |
111
+ | **seasons** | Сезоны | `list`, `get_current`, `get_by_name`, `get_season_id_by_name` |
112
+ | **fields** | Поля | `list_()`, `get_contours`, `get_phenophases`, `get_monitoring`, `list_meta` |
113
+ | **tasks** | Задачи | `list_()`, `create`, `get_list_meta` |
114
+ | **reports** | Отчёты | `list_()`, `create`, `get_metadata`, `get_problems`, `get_list_meta` |
115
+ | **techmaps** | Технологические карты | `list_()`, `create` |
116
+ | **meteostations** | Метеостанции | `list_()`, `get`, `get_by_field`, `get_manufacturers`, `get_providers`, `get_sensors`, `get_coordinates_history`, `get_history` |
117
+ | **dictionaries** | Справочники | `get_crops`, `get_crop_products`, `get_techoperations`, `get_pesticides`, `get_fertilizers`, `get_machines`, `get_tools`, `get_units`, `get_regions`, `get_countries`, и др. |
118
+
119
+ ## Примеры
120
+
121
+ Скопируйте `.env.example` в `.env` и заполните переменные:
122
+
123
+ ```bash
124
+ cp .env.example .env
125
+ ```
126
+
127
+ ```env
128
+ ASSISTAGRO_EMAIL=user@example.com
129
+ ASSISTAGRO_PASSWORD=your-password
130
+ ASSISTAGRO_BASE_URL=https://gateway-frontend.agroassist.ru
131
+ ASSISTAGRO_DEBUG=false
132
+ ```
133
+
134
+ Запуск примеров:
135
+
136
+ ```bash
137
+ # Запустить все модули
138
+ uv run python examples/run_tests.py
139
+
140
+ # Запустить конкретный модуль
141
+ uv run python examples/run_tests.py --module structures
142
+ uv run python examples/run_tests.py -m auth
143
+
144
+ # Доступные модули:
145
+ # auth, structures, companies, current_user, users,
146
+ # dictionaries, meteostations, tasks, fields, reports, techmaps, seasons
147
+ ```
148
+
149
+ ## Разработка
150
+
151
+ ```bash
152
+ # Установить зависимости
153
+ uv sync
154
+
155
+ # Запустить тесты
156
+ uv run pytest
157
+
158
+ # Запустить линтер
159
+ uv run ruff check .
160
+ uv run ruff check --fix .
161
+
162
+ # Проверка типов
163
+ uv run mypy src/
164
+
165
+ # Форматирование
166
+ uv run black .
167
+ ```
168
+
169
+ ## Лицензия
170
+
171
+ MIT
@@ -0,0 +1,155 @@
1
+ # AssistAgro API Client
2
+
3
+ Синхронный и асинхронный HTTP-клиент для AssistAgro API.
4
+
5
+ ## Установка
6
+
7
+ ```bash
8
+ pip install assistagro-client
9
+ ```
10
+
11
+ ## Асинхронное использование
12
+
13
+ ```python
14
+ import asyncio
15
+ from assistagro_client import AssistAgroClient
16
+
17
+ async def main():
18
+ async with AssistAgroClient(base_url="https://gateway-frontend.agroassist.ru") as client:
19
+ tokens = await client.auth.sign_in(
20
+ email="user@example.com",
21
+ password="password123"
22
+ )
23
+ print(f"Access token: {tokens.access_token[:20]}...")
24
+
25
+ structures = await client.structures.list_()
26
+ structure_guid = structures[0].structure_guid
27
+
28
+ season = await client.seasons.get_current()
29
+
30
+ fields = await client.fields.list_(
31
+ structure_guids=[structure_guid],
32
+ season_id=season.season_id,
33
+ )
34
+ print(f"Найдено полей: {len(fields)}")
35
+
36
+ crops_response = await client.dictionaries.get_crops()
37
+ print(f"Культур: {len(crops_response.crops)}")
38
+
39
+ tasks = await client.tasks.list_(structure_guid=structure_guid)
40
+ print(f"Найдено задач: {len(tasks)}")
41
+
42
+ if __name__ == "__main__":
43
+ asyncio.run(main())
44
+ ```
45
+
46
+ ## Синхронное использование
47
+
48
+ ```python
49
+ from assistagro_client import AssistAgroSyncClient
50
+
51
+ with AssistAgroSyncClient(base_url="https://gateway-frontend.agroassist.ru") as client:
52
+ tokens = client.auth.sign_in(
53
+ email="user@example.com",
54
+ password="password123",
55
+ )
56
+ print(f"Access token: {tokens.access_token[:20]}...")
57
+
58
+ structures = client.structures.list_()
59
+ print(f"Найдено структур: {len(structures)}")
60
+ ```
61
+
62
+ `AssistAgroClient` остается асинхронным клиентом для обратной совместимости.
63
+ `AssistAgroSyncClient` предоставляет тот же набор API-разделов для обычного синхронного кода.
64
+ Если нужно передать контекст пользователя в `x-user-data`, используйте `set_user_data(...)`;
65
+ заголовок сериализуется как компактный JSON, например
66
+ `{"company_guid":"...","user_guid":"..."}`.
67
+
68
+ ## TLS/SSL сертификаты
69
+
70
+ По умолчанию клиент проверяет TLS-сертификат сервера. Если окружение использует свой
71
+ CA-bundle, передайте путь к нему:
72
+
73
+ ```python
74
+ client = AssistAgroClient(verify="/path/to/ca-bundle.pem")
75
+ ```
76
+
77
+ Если сервер временно отдает некорректную цепочку сертификатов, проверку можно отключить
78
+ явно:
79
+
80
+ ```python
81
+ client = AssistAgroClient(verify=False)
82
+ ```
83
+
84
+ То же доступно для `AssistAgroSyncClient` и через переменную окружения
85
+ `ASSISTAGRO_VERIFY_SSL=false`.
86
+
87
+ ## API эндпоинты
88
+
89
+ | Модуль | Описание | Основные методы |
90
+ |--------|----------|-----------------|
91
+ | **auth** | Аутентификация | `sign_in`, `refresh_tokens`, `logout` |
92
+ | **accounts** | Пользователи | `get_current_user`, `get_users` |
93
+ | **companies** | Компании | `list_()`, `list_all`, `get`, `get_license`, `get_license_metadata`, `get_activities`, `get_dashboards` |
94
+ | **structures** | Структуры | `list_()`, `list_plain` |
95
+ | **seasons** | Сезоны | `list`, `get_current`, `get_by_name`, `get_season_id_by_name` |
96
+ | **fields** | Поля | `list_()`, `get_contours`, `get_phenophases`, `get_monitoring`, `list_meta` |
97
+ | **tasks** | Задачи | `list_()`, `create`, `get_list_meta` |
98
+ | **reports** | Отчёты | `list_()`, `create`, `get_metadata`, `get_problems`, `get_list_meta` |
99
+ | **techmaps** | Технологические карты | `list_()`, `create` |
100
+ | **meteostations** | Метеостанции | `list_()`, `get`, `get_by_field`, `get_manufacturers`, `get_providers`, `get_sensors`, `get_coordinates_history`, `get_history` |
101
+ | **dictionaries** | Справочники | `get_crops`, `get_crop_products`, `get_techoperations`, `get_pesticides`, `get_fertilizers`, `get_machines`, `get_tools`, `get_units`, `get_regions`, `get_countries`, и др. |
102
+
103
+ ## Примеры
104
+
105
+ Скопируйте `.env.example` в `.env` и заполните переменные:
106
+
107
+ ```bash
108
+ cp .env.example .env
109
+ ```
110
+
111
+ ```env
112
+ ASSISTAGRO_EMAIL=user@example.com
113
+ ASSISTAGRO_PASSWORD=your-password
114
+ ASSISTAGRO_BASE_URL=https://gateway-frontend.agroassist.ru
115
+ ASSISTAGRO_DEBUG=false
116
+ ```
117
+
118
+ Запуск примеров:
119
+
120
+ ```bash
121
+ # Запустить все модули
122
+ uv run python examples/run_tests.py
123
+
124
+ # Запустить конкретный модуль
125
+ uv run python examples/run_tests.py --module structures
126
+ uv run python examples/run_tests.py -m auth
127
+
128
+ # Доступные модули:
129
+ # auth, structures, companies, current_user, users,
130
+ # dictionaries, meteostations, tasks, fields, reports, techmaps, seasons
131
+ ```
132
+
133
+ ## Разработка
134
+
135
+ ```bash
136
+ # Установить зависимости
137
+ uv sync
138
+
139
+ # Запустить тесты
140
+ uv run pytest
141
+
142
+ # Запустить линтер
143
+ uv run ruff check .
144
+ uv run ruff check --fix .
145
+
146
+ # Проверка типов
147
+ uv run mypy src/
148
+
149
+ # Форматирование
150
+ uv run black .
151
+ ```
152
+
153
+ ## Лицензия
154
+
155
+ MIT
@@ -0,0 +1,39 @@
1
+ [project]
2
+ name = "assistagro-client"
3
+ version = "0.3.1"
4
+ description = "HTTP client for AssistAgro API"
5
+ readme = "README.md"
6
+ requires-python = ">=3.10"
7
+ license = "MIT"
8
+ authors = [
9
+ {name = "Dmitriy Kazakov", email = "dmitriyfile@yandex.ru"},
10
+ ]
11
+ classifiers = [
12
+ "Programming Language :: Python :: 3",
13
+ "Programming Language :: Python :: 3.13",
14
+ ]
15
+ dependencies = [
16
+ "httpx>=0.28.1",
17
+ "pydantic>=2.12.5",
18
+ "pydantic-settings>=2.13.1",
19
+ "python-dateutil>=2.9.0.post0",
20
+ "python-dotenv>=1.0.0",
21
+ ]
22
+
23
+ [dependency-groups]
24
+ dev = [
25
+ "pytest>=9.0.2",
26
+ "pytest-asyncio>=1.3.0",
27
+ "ruff>=0.9.0",
28
+ "mypy>=1.15.0",
29
+ ]
30
+
31
+ [build-system]
32
+ requires = ["setuptools>=68"]
33
+ build-backend = "setuptools.build_meta"
34
+
35
+ [tool.setuptools.packages.find]
36
+ where = ["src"]
37
+
38
+ [tool.pytest.ini_options]
39
+ pythonpath = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,20 @@
1
+ """AssistAgro API Client."""
2
+
3
+ from .client import AssistAgroClient, AssistAgroSyncClient
4
+ from .auth import Auth, AuthTokens, SignInRequest, OTPResponse
5
+ from .exceptions import AssistAgroError, AuthenticationError, APIError, TimeoutError
6
+ from .api.v1.seasons import Season
7
+
8
+ __all__ = [
9
+ "AssistAgroClient",
10
+ "AssistAgroSyncClient",
11
+ "Auth",
12
+ "AuthTokens",
13
+ "SignInRequest",
14
+ "OTPResponse",
15
+ "AssistAgroError",
16
+ "AuthenticationError",
17
+ "APIError",
18
+ "TimeoutError",
19
+ "Season",
20
+ ]
@@ -0,0 +1 @@
1
+ """API v1 endpoints."""
@@ -0,0 +1 @@
1
+ """API v1 endpoints."""
@@ -0,0 +1,42 @@
1
+ """Accounts API endpoints."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING
6
+ from uuid import UUID
7
+
8
+ from assistagro_client.models.accounts import UserProfile
9
+
10
+ if TYPE_CHECKING:
11
+ from assistagro_client.client import AssistAgroClient
12
+
13
+
14
+ class AccountsAPI:
15
+ """Accounts API endpoints."""
16
+
17
+ def __init__(self, client: AssistAgroClient):
18
+ self._client = client
19
+
20
+ async def get_users(self, account_guid: UUID | None = None) -> list[UserProfile]:
21
+ """Get user profiles."""
22
+ params = {}
23
+ if account_guid:
24
+ params["account_guid"] = str(account_guid)
25
+ response = await self._client.get("/account/users", params=params)
26
+ response.raise_for_status()
27
+ data = self._client._parse_json_response(response)
28
+ return [UserProfile(**item) for item in data] if data else []
29
+
30
+ async def get_current_user(self) -> dict:
31
+ """Get current user info."""
32
+ response = await self._client.get("/account/users/current")
33
+ response.raise_for_status()
34
+ data = self._client._parse_json_response(response)
35
+ return data if data else {}
36
+
37
+ async def get_user(self, user_guid: UUID) -> dict:
38
+ """Get user by GUID."""
39
+ response = await self._client.get(f"/account/users/{user_guid}")
40
+ response.raise_for_status()
41
+ data = self._client._parse_json_response(response)
42
+ return data if data else {}
@@ -0,0 +1,108 @@
1
+ """Companies API endpoints."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING, Any, Optional
6
+ from uuid import UUID
7
+
8
+ from assistagro_client.models.companies import Company, CompanyLicense
9
+
10
+ if TYPE_CHECKING:
11
+ from assistagro_client.client import AssistAgroClient
12
+
13
+
14
+ class CompaniesAPI:
15
+ """Companies API endpoints."""
16
+
17
+ def __init__(self, client: AssistAgroClient):
18
+ self._client = client
19
+
20
+ async def list_(self) -> list[Company]:
21
+ """Get current user's company.
22
+
23
+ Returns the company associated with the authenticated user.
24
+ """
25
+ response = await self._client.get("/account/users/current")
26
+ response.raise_for_status()
27
+ data = self._client._parse_json_response(response)
28
+ if isinstance(data, dict) and "company" in data:
29
+ company_data = data["company"]
30
+ return [Company(**company_data)] if company_data else []
31
+ return []
32
+
33
+ async def list_all(self) -> list[dict[str, Any]]:
34
+ """Get all companies (requires superadmin permission 1).
35
+
36
+ Returns:
37
+ List of company objects with guid, name, user_count, active_flag, license.
38
+ """
39
+ response = await self._client.get("/companies")
40
+ response.raise_for_status()
41
+ data = self._client._parse_json_response(response)
42
+ return data if isinstance(data, list) else []
43
+
44
+ async def get(self, company_guid: UUID) -> dict[str, Any]:
45
+ """Get company by GUID.
46
+
47
+ Args:
48
+ company_guid: Company GUID.
49
+
50
+ Returns:
51
+ Company data with guid, name, user_count, active_flag.
52
+ """
53
+ response = await self._client.get(f"/companies/{company_guid}")
54
+ response.raise_for_status()
55
+ return self._client._parse_json_response(response)
56
+
57
+ async def get_license(
58
+ self, company_guid: Optional[UUID] = None
59
+ ) -> CompanyLicense:
60
+ """Get company license info.
61
+
62
+ Args:
63
+ company_guid: Optional company GUID. If not provided, uses token's company.
64
+
65
+ Returns:
66
+ Company license information.
67
+ """
68
+ params: dict[str, Any] = {}
69
+ if company_guid:
70
+ params["company_guid"] = str(company_guid)
71
+ response = await self._client.get("/companies/license", params=params)
72
+ response.raise_for_status()
73
+ data = self._client._parse_json_response(response)
74
+ return CompanyLicense(**data)
75
+
76
+ async def get_license_metadata(self) -> dict[str, Any]:
77
+ """Get license metadata (modules, permissions, license types, limits).
78
+
79
+ Returns:
80
+ Dict with modules, permissions, permission_groups, license_types, license_limits.
81
+ """
82
+ response = await self._client.get("/companies/license/metadata")
83
+ response.raise_for_status()
84
+ return self._client._parse_json_response(response)
85
+
86
+ async def get_activities(self) -> list[dict[str, Any]]:
87
+ """Get company activity types.
88
+
89
+ Returns:
90
+ List of company activities with id and description.
91
+ """
92
+ response = await self._client.get("/companies/activities")
93
+ response.raise_for_status()
94
+ data = self._client._parse_json_response(response)
95
+ if isinstance(data, dict) and "company_activities" in data:
96
+ return data["company_activities"]
97
+ return []
98
+
99
+ async def get_dashboards(self) -> list[dict[str, Any]]:
100
+ """Get company dashboards (requires permission 55).
101
+
102
+ Returns:
103
+ List of dashboard objects with id and name.
104
+ """
105
+ response = await self._client.get("/companies/dashboards")
106
+ response.raise_for_status()
107
+ data = self._client._parse_json_response(response)
108
+ return data if isinstance(data, list) else []