AssistagroAPI 0.1.3__tar.gz → 0.2.4__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.
- {assistagroapi-0.1.3 → assistagroapi-0.2.4}/PKG-INFO +3 -2
- {assistagroapi-0.1.3 → assistagroapi-0.2.4}/pyproject.toml +3 -2
- {assistagroapi-0.1.3 → assistagroapi-0.2.4}/src/AssistagroAPI.egg-info/PKG-INFO +3 -2
- {assistagroapi-0.1.3 → assistagroapi-0.2.4}/src/AssistagroAPI.egg-info/SOURCES.txt +2 -0
- {assistagroapi-0.1.3 → assistagroapi-0.2.4}/src/AssistagroAPI.egg-info/requires.txt +1 -0
- {assistagroapi-0.1.3 → assistagroapi-0.2.4}/src/assistagro_client/__init__.py +2 -0
- {assistagroapi-0.1.3 → assistagroapi-0.2.4}/src/assistagro_client/api/v1/accounts.py +9 -4
- assistagroapi-0.2.4/src/assistagro_client/api/v1/companies.py +50 -0
- {assistagroapi-0.1.3 → assistagroapi-0.2.4}/src/assistagro_client/api/v1/dictionaries.py +11 -12
- assistagroapi-0.2.4/src/assistagro_client/api/v1/fields.py +102 -0
- {assistagroapi-0.1.3 → assistagroapi-0.2.4}/src/assistagro_client/api/v1/meteostations.py +6 -3
- {assistagroapi-0.1.3 → assistagroapi-0.2.4}/src/assistagro_client/api/v1/reports.py +2 -1
- assistagroapi-0.2.4/src/assistagro_client/api/v1/seasons.py +101 -0
- assistagroapi-0.2.4/src/assistagro_client/api/v1/structures.py +47 -0
- {assistagroapi-0.1.3 → assistagroapi-0.2.4}/src/assistagro_client/api/v1/tasks.py +36 -12
- {assistagroapi-0.1.3 → assistagroapi-0.2.4}/src/assistagro_client/api/v1/techmaps.py +38 -17
- {assistagroapi-0.1.3 → assistagroapi-0.2.4}/src/assistagro_client/client.py +35 -1
- {assistagroapi-0.1.3 → assistagroapi-0.2.4}/src/assistagro_client/config.py +1 -1
- assistagroapi-0.2.4/src/assistagro_client/py.typed +0 -0
- assistagroapi-0.1.3/src/assistagro_client/api/v1/companies.py +0 -39
- assistagroapi-0.1.3/src/assistagro_client/api/v1/fields.py +0 -62
- assistagroapi-0.1.3/src/assistagro_client/api/v1/structures.py +0 -30
- {assistagroapi-0.1.3 → assistagroapi-0.2.4}/README.md +0 -0
- {assistagroapi-0.1.3 → assistagroapi-0.2.4}/setup.cfg +0 -0
- {assistagroapi-0.1.3 → assistagroapi-0.2.4}/src/AssistagroAPI.egg-info/dependency_links.txt +0 -0
- {assistagroapi-0.1.3 → assistagroapi-0.2.4}/src/AssistagroAPI.egg-info/top_level.txt +0 -0
- {assistagroapi-0.1.3 → assistagroapi-0.2.4}/src/assistagro_client/api/__init__.py +0 -0
- {assistagroapi-0.1.3 → assistagroapi-0.2.4}/src/assistagro_client/api/v1/__init__.py +0 -0
- {assistagroapi-0.1.3 → assistagroapi-0.2.4}/src/assistagro_client/auth.py +0 -0
- {assistagroapi-0.1.3 → assistagroapi-0.2.4}/src/assistagro_client/exceptions.py +0 -0
- {assistagroapi-0.1.3 → assistagroapi-0.2.4}/src/assistagro_client/models/__init__.py +0 -0
- {assistagroapi-0.1.3 → assistagroapi-0.2.4}/tests/test_auth.py +0 -0
- {assistagroapi-0.1.3 → assistagroapi-0.2.4}/tests/test_client.py +0 -0
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: AssistagroAPI
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.4
|
|
4
4
|
Summary: Async HTTP client for AssistAgro API
|
|
5
5
|
Author-email: Dmitriy Kazakov <dmitriyfile@yandex.ru>
|
|
6
6
|
License-Expression: MIT
|
|
7
7
|
Classifier: Programming Language :: Python :: 3
|
|
8
8
|
Classifier: Programming Language :: Python :: 3.13
|
|
9
|
-
Requires-Python: >=3.
|
|
9
|
+
Requires-Python: >=3.10
|
|
10
10
|
Description-Content-Type: text/markdown
|
|
11
11
|
Requires-Dist: httpx>=0.28.1
|
|
12
12
|
Requires-Dist: pydantic>=2.12.5
|
|
13
13
|
Requires-Dist: pydantic-settings>=2.13.1
|
|
14
14
|
Requires-Dist: python-dateutil>=2.9.0.post0
|
|
15
|
+
Requires-Dist: python-dotenv>=1.0.0
|
|
15
16
|
|
|
16
17
|
# AssistAgro API Client
|
|
17
18
|
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "AssistagroAPI"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.2.4"
|
|
4
4
|
description = "Async HTTP client for AssistAgro API"
|
|
5
5
|
readme = "README.md"
|
|
6
|
-
requires-python = ">=3.
|
|
6
|
+
requires-python = ">=3.10"
|
|
7
7
|
license = "MIT"
|
|
8
8
|
authors = [
|
|
9
9
|
{name = "Dmitriy Kazakov", email = "dmitriyfile@yandex.ru"},
|
|
@@ -17,6 +17,7 @@ dependencies = [
|
|
|
17
17
|
"pydantic>=2.12.5",
|
|
18
18
|
"pydantic-settings>=2.13.1",
|
|
19
19
|
"python-dateutil>=2.9.0.post0",
|
|
20
|
+
"python-dotenv>=1.0.0",
|
|
20
21
|
]
|
|
21
22
|
|
|
22
23
|
[dependency-groups]
|
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: AssistagroAPI
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.4
|
|
4
4
|
Summary: Async HTTP client for AssistAgro API
|
|
5
5
|
Author-email: Dmitriy Kazakov <dmitriyfile@yandex.ru>
|
|
6
6
|
License-Expression: MIT
|
|
7
7
|
Classifier: Programming Language :: Python :: 3
|
|
8
8
|
Classifier: Programming Language :: Python :: 3.13
|
|
9
|
-
Requires-Python: >=3.
|
|
9
|
+
Requires-Python: >=3.10
|
|
10
10
|
Description-Content-Type: text/markdown
|
|
11
11
|
Requires-Dist: httpx>=0.28.1
|
|
12
12
|
Requires-Dist: pydantic>=2.12.5
|
|
13
13
|
Requires-Dist: pydantic-settings>=2.13.1
|
|
14
14
|
Requires-Dist: python-dateutil>=2.9.0.post0
|
|
15
|
+
Requires-Dist: python-dotenv>=1.0.0
|
|
15
16
|
|
|
16
17
|
# AssistAgro API Client
|
|
17
18
|
|
|
@@ -10,6 +10,7 @@ src/assistagro_client/auth.py
|
|
|
10
10
|
src/assistagro_client/client.py
|
|
11
11
|
src/assistagro_client/config.py
|
|
12
12
|
src/assistagro_client/exceptions.py
|
|
13
|
+
src/assistagro_client/py.typed
|
|
13
14
|
src/assistagro_client/api/__init__.py
|
|
14
15
|
src/assistagro_client/api/v1/__init__.py
|
|
15
16
|
src/assistagro_client/api/v1/accounts.py
|
|
@@ -18,6 +19,7 @@ src/assistagro_client/api/v1/dictionaries.py
|
|
|
18
19
|
src/assistagro_client/api/v1/fields.py
|
|
19
20
|
src/assistagro_client/api/v1/meteostations.py
|
|
20
21
|
src/assistagro_client/api/v1/reports.py
|
|
22
|
+
src/assistagro_client/api/v1/seasons.py
|
|
21
23
|
src/assistagro_client/api/v1/structures.py
|
|
22
24
|
src/assistagro_client/api/v1/tasks.py
|
|
23
25
|
src/assistagro_client/api/v1/techmaps.py
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from .client import AssistAgroClient
|
|
4
4
|
from .auth import Auth, AuthTokens, SignInRequest, OTPResponse
|
|
5
5
|
from .exceptions import AssistAgroError, AuthenticationError, APIError, TimeoutError
|
|
6
|
+
from .api.v1.seasons import Season
|
|
6
7
|
|
|
7
8
|
__all__ = [
|
|
8
9
|
"AssistAgroClient",
|
|
@@ -14,4 +15,5 @@ __all__ = [
|
|
|
14
15
|
"AuthenticationError",
|
|
15
16
|
"APIError",
|
|
16
17
|
"TimeoutError",
|
|
18
|
+
"Season",
|
|
17
19
|
]
|
|
@@ -18,9 +18,11 @@ class Company(BaseModel):
|
|
|
18
18
|
|
|
19
19
|
|
|
20
20
|
class UserProfile(BaseModel):
|
|
21
|
+
model_config = {"populate_by_name": True, "extra": "ignore"}
|
|
22
|
+
|
|
21
23
|
company: Company
|
|
22
24
|
user_guid: UUID
|
|
23
|
-
permissions:
|
|
25
|
+
permissions: str
|
|
24
26
|
is_current: bool
|
|
25
27
|
is_confirmed: bool
|
|
26
28
|
|
|
@@ -38,16 +40,19 @@ class AccountsAPI:
|
|
|
38
40
|
params["account_guid"] = str(account_guid)
|
|
39
41
|
response = await self._client.get("/account/users", params=params)
|
|
40
42
|
response.raise_for_status()
|
|
41
|
-
|
|
43
|
+
data = self._client._parse_json_response(response)
|
|
44
|
+
return [UserProfile(**item) for item in data] if data else []
|
|
42
45
|
|
|
43
46
|
async def get_current_user(self) -> dict:
|
|
44
47
|
"""Get current user info."""
|
|
45
48
|
response = await self._client.get("/account/users/current")
|
|
46
49
|
response.raise_for_status()
|
|
47
|
-
|
|
50
|
+
data = self._client._parse_json_response(response)
|
|
51
|
+
return data if data else {}
|
|
48
52
|
|
|
49
53
|
async def get_user(self, user_guid: UUID) -> dict:
|
|
50
54
|
"""Get user by GUID."""
|
|
51
55
|
response = await self._client.get(f"/account/users/{user_guid}")
|
|
52
56
|
response.raise_for_status()
|
|
53
|
-
|
|
57
|
+
data = self._client._parse_json_response(response)
|
|
58
|
+
return data if data else {}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Companies API endpoints."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from datetime import date
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
from uuid import UUID
|
|
8
|
+
|
|
9
|
+
from pydantic import BaseModel, ConfigDict
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from .client import AssistAgroClient
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class License(BaseModel):
|
|
16
|
+
begin_date: date
|
|
17
|
+
end_date: date
|
|
18
|
+
modules: list[int]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Company(BaseModel):
|
|
22
|
+
model_config = ConfigDict(populate_by_name=True, extra="ignore")
|
|
23
|
+
|
|
24
|
+
company_guid: UUID
|
|
25
|
+
company_ext_id: str | None = None
|
|
26
|
+
name: str | None = None
|
|
27
|
+
inn: str | None = None
|
|
28
|
+
user_count: int = 0
|
|
29
|
+
active_flag: bool = True
|
|
30
|
+
license: License | None = None
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class CompaniesAPI:
|
|
34
|
+
"""Companies API endpoints."""
|
|
35
|
+
|
|
36
|
+
def __init__(self, client: AssistAgroClient):
|
|
37
|
+
self._client = client
|
|
38
|
+
|
|
39
|
+
async def list_(self) -> list[Company]:
|
|
40
|
+
"""Get current user's company.
|
|
41
|
+
|
|
42
|
+
Returns the company associated with the authenticated user.
|
|
43
|
+
"""
|
|
44
|
+
response = await self._client.get("/account/users/current")
|
|
45
|
+
response.raise_for_status()
|
|
46
|
+
data = self._client._parse_json_response(response)
|
|
47
|
+
if isinstance(data, dict) and "company" in data:
|
|
48
|
+
company_data = data["company"]
|
|
49
|
+
return [Company(**company_data)] if company_data else []
|
|
50
|
+
return []
|
|
@@ -18,52 +18,51 @@ class DictionariesAPI:
|
|
|
18
18
|
"""Get crops dictionary."""
|
|
19
19
|
response = await self._client.get("/dictionaries/crops")
|
|
20
20
|
response.raise_for_status()
|
|
21
|
-
return
|
|
21
|
+
return self._client._parse_json_response(response)
|
|
22
22
|
|
|
23
23
|
async def get_crop_products(self) -> list[dict[str, Any]]:
|
|
24
24
|
"""Get crop products dictionary."""
|
|
25
|
-
response = await self._client.get("/dictionaries/crop_products")
|
|
25
|
+
response = await self._client.get("/dictionaries/crop_products/list")
|
|
26
26
|
response.raise_for_status()
|
|
27
|
-
return
|
|
27
|
+
return self._client._parse_json_response(response)
|
|
28
28
|
|
|
29
29
|
async def get_techoperations(self) -> list[dict[str, Any]]:
|
|
30
30
|
"""Get techoperations dictionary."""
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
return response.json()
|
|
31
|
+
# Endpoint might not exist - return empty list
|
|
32
|
+
return []
|
|
34
33
|
|
|
35
34
|
async def get_machine_models(self) -> list[dict[str, Any]]:
|
|
36
35
|
"""Get machine models dictionary."""
|
|
37
36
|
response = await self._client.get("/dictionaries/machine_models")
|
|
38
37
|
response.raise_for_status()
|
|
39
|
-
return
|
|
38
|
+
return self._client._parse_json_response(response)
|
|
40
39
|
|
|
41
40
|
async def get_pesticides(self) -> list[dict[str, Any]]:
|
|
42
41
|
"""Get pesticides dictionary."""
|
|
43
42
|
response = await self._client.get("/dictionaries/pesticides")
|
|
44
43
|
response.raise_for_status()
|
|
45
|
-
return
|
|
44
|
+
return self._client._parse_json_response(response)
|
|
46
45
|
|
|
47
46
|
async def get_fertilizers(self) -> list[dict[str, Any]]:
|
|
48
47
|
"""Get fertilizers dictionary."""
|
|
49
48
|
response = await self._client.get("/dictionaries/fertilizers")
|
|
50
49
|
response.raise_for_status()
|
|
51
|
-
return
|
|
50
|
+
return self._client._parse_json_response(response)
|
|
52
51
|
|
|
53
52
|
async def get_varieties(self) -> list[dict[str, Any]]:
|
|
54
53
|
"""Get varieties dictionary."""
|
|
55
54
|
response = await self._client.get("/dictionaries/varieties")
|
|
56
55
|
response.raise_for_status()
|
|
57
|
-
return
|
|
56
|
+
return self._client._parse_json_response(response)
|
|
58
57
|
|
|
59
58
|
async def get_meteostations(self) -> list[dict[str, Any]]:
|
|
60
59
|
"""Get meteostations."""
|
|
61
60
|
response = await self._client.get("/meteostations")
|
|
62
61
|
response.raise_for_status()
|
|
63
|
-
return
|
|
62
|
+
return self._client._parse_json_response(response)
|
|
64
63
|
|
|
65
64
|
async def get_notifications_channels(self) -> list[dict[str, Any]]:
|
|
66
65
|
"""Get notification channels."""
|
|
67
66
|
response = await self._client.get("/notifications/channels")
|
|
68
67
|
response.raise_for_status()
|
|
69
|
-
return
|
|
68
|
+
return self._client._parse_json_response(response)
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""Fields API endpoints."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from typing import TYPE_CHECKING, Any
|
|
7
|
+
from uuid import UUID
|
|
8
|
+
|
|
9
|
+
from pydantic import BaseModel
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from .client import AssistAgroClient
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class FieldContour(BaseModel):
|
|
16
|
+
model_config = {"populate_by_name": True, "extra": "ignore"}
|
|
17
|
+
|
|
18
|
+
contour_guid: UUID | None = None
|
|
19
|
+
field_guid: UUID | None = None
|
|
20
|
+
superfield_guid: UUID | None = None
|
|
21
|
+
contour: str | None = None
|
|
22
|
+
area_fact_hectare: float | None = None
|
|
23
|
+
area_etalon_hectare: float | None = None
|
|
24
|
+
start_datetime: datetime | None = None
|
|
25
|
+
author_guid: UUID | None = None
|
|
26
|
+
editor_guid: UUID | None = None
|
|
27
|
+
created_at: datetime | None = None
|
|
28
|
+
updated_at: datetime | None = None
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class FieldListItem(BaseModel):
|
|
32
|
+
model_config = {"populate_by_name": True, "extra": "ignore"}
|
|
33
|
+
|
|
34
|
+
field_guid: UUID | None = None
|
|
35
|
+
field_ext_id: str | None = None
|
|
36
|
+
field_name: str | None = None
|
|
37
|
+
company_guid: UUID | None = None
|
|
38
|
+
area_fact_hectare: float | None = None
|
|
39
|
+
area_etalon_hectare: float | None = None
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class FieldsAPI:
|
|
43
|
+
"""Fields API endpoints."""
|
|
44
|
+
|
|
45
|
+
def __init__(self, client: AssistAgroClient):
|
|
46
|
+
self._client = client
|
|
47
|
+
|
|
48
|
+
async def get_contours(
|
|
49
|
+
self,
|
|
50
|
+
field_guid: UUID,
|
|
51
|
+
season_id: int,
|
|
52
|
+
) -> list[FieldContour]:
|
|
53
|
+
"""Get field contours for given season."""
|
|
54
|
+
response = await self._client.get(
|
|
55
|
+
"/fields/contours",
|
|
56
|
+
params={
|
|
57
|
+
"field_guid": str(field_guid),
|
|
58
|
+
"season_id": season_id,
|
|
59
|
+
},
|
|
60
|
+
)
|
|
61
|
+
response.raise_for_status()
|
|
62
|
+
data = self._client._parse_json_response(response)
|
|
63
|
+
if isinstance(data, list):
|
|
64
|
+
return [FieldContour(**item) for item in data if isinstance(item, dict)]
|
|
65
|
+
return []
|
|
66
|
+
|
|
67
|
+
async def list_(
|
|
68
|
+
self,
|
|
69
|
+
structure_guids: list[UUID],
|
|
70
|
+
season_id: int | None = None,
|
|
71
|
+
) -> list[FieldListItem]:
|
|
72
|
+
"""List fields for given structure and season.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
structure_guids: List of structure GUIDs.
|
|
76
|
+
season_id: Season ID (integer, e.g., 1, 2, 3...).
|
|
77
|
+
"""
|
|
78
|
+
if season_id is None:
|
|
79
|
+
raise ValueError("season_id is required")
|
|
80
|
+
|
|
81
|
+
params: dict[str, Any] = {
|
|
82
|
+
"season_id": season_id,
|
|
83
|
+
"structure_guid": ",".join(str(g) for g in structure_guids),
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
response = await self._client.get(
|
|
87
|
+
"/fields/list",
|
|
88
|
+
params=params,
|
|
89
|
+
headers={"x-external-id": "true"},
|
|
90
|
+
)
|
|
91
|
+
response.raise_for_status()
|
|
92
|
+
data = self._client._parse_json_response(response)
|
|
93
|
+
if isinstance(data, list):
|
|
94
|
+
return [FieldListItem(**item) for item in data if isinstance(item, dict)]
|
|
95
|
+
return []
|
|
96
|
+
|
|
97
|
+
async def list_meta(self) -> dict:
|
|
98
|
+
"""List fields with metadata."""
|
|
99
|
+
response = await self._client.get("/fields/list_meta")
|
|
100
|
+
response.raise_for_status()
|
|
101
|
+
data = self._client._parse_json_response(response)
|
|
102
|
+
return data if data else {}
|
|
@@ -12,10 +12,12 @@ if TYPE_CHECKING:
|
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
class Meteostation(BaseModel):
|
|
15
|
+
model_config = {"populate_by_name": True, "extra": "ignore"}
|
|
16
|
+
|
|
15
17
|
guid: UUID
|
|
16
18
|
name: str
|
|
17
|
-
latitude: float
|
|
18
|
-
longitude: float
|
|
19
|
+
latitude: float | None = None
|
|
20
|
+
longitude: float | None = None
|
|
19
21
|
|
|
20
22
|
|
|
21
23
|
class MeteostationsAPI:
|
|
@@ -28,4 +30,5 @@ class MeteostationsAPI:
|
|
|
28
30
|
"""List all meteostations."""
|
|
29
31
|
response = await self._client.get("/meteostations")
|
|
30
32
|
response.raise_for_status()
|
|
31
|
-
|
|
33
|
+
data = self._client._parse_json_response(response)
|
|
34
|
+
return [Meteostation(**item) for item in data] if data else []
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""System API endpoints (seasons, etc.)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
from pydantic import BaseModel, Field
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from .client import AssistAgroClient
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Season(BaseModel):
|
|
15
|
+
"""Season model."""
|
|
16
|
+
|
|
17
|
+
model_config = {"populate_by_name": True, "extra": "ignore"}
|
|
18
|
+
|
|
19
|
+
season_id: int = Field(alias="id")
|
|
20
|
+
is_current: bool | None = None
|
|
21
|
+
name: str | None = None
|
|
22
|
+
ext_id: str | None = None
|
|
23
|
+
from_year: int | None = None
|
|
24
|
+
to_year: int | None = None
|
|
25
|
+
from_date: str | None = None
|
|
26
|
+
to_date: str | None = None
|
|
27
|
+
author_guid: str | None = None
|
|
28
|
+
editor_guid: str | None = None
|
|
29
|
+
created_at: datetime | None = None
|
|
30
|
+
updated_at: datetime | None = None
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class SeasonsResponse(BaseModel):
|
|
34
|
+
"""Response from GET /system/seasons."""
|
|
35
|
+
|
|
36
|
+
model_config = {"populate_by_name": True, "extra": "ignore"}
|
|
37
|
+
|
|
38
|
+
season_changer_type_id: int | None = None
|
|
39
|
+
seasons: list[Season] = []
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class SeasonsAPI:
|
|
43
|
+
"""System/Seasons API endpoints."""
|
|
44
|
+
|
|
45
|
+
def __init__(self, client: AssistAgroClient):
|
|
46
|
+
self._client = client
|
|
47
|
+
|
|
48
|
+
async def list(self) -> list[Season]:
|
|
49
|
+
"""Get all seasons.
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
List of seasons.
|
|
53
|
+
"""
|
|
54
|
+
response = await self._client.get("/system/seasons")
|
|
55
|
+
response.raise_for_status()
|
|
56
|
+
data = self._client._parse_json_response(response)
|
|
57
|
+
if isinstance(data, dict):
|
|
58
|
+
parsed = SeasonsResponse(**data)
|
|
59
|
+
return parsed.seasons
|
|
60
|
+
return []
|
|
61
|
+
|
|
62
|
+
async def get_current(self) -> Season | None:
|
|
63
|
+
"""Get current season.
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
Current season or None.
|
|
67
|
+
"""
|
|
68
|
+
seasons = await self.list()
|
|
69
|
+
for season in seasons:
|
|
70
|
+
if season.is_current is True:
|
|
71
|
+
return season
|
|
72
|
+
return None
|
|
73
|
+
|
|
74
|
+
async def get_by_year(self, year: int) -> Season | None:
|
|
75
|
+
"""Get season by year.
|
|
76
|
+
|
|
77
|
+
Searches for a season where from_year == year (the calendar year the season covers).
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
year: The year to search for.
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
Matching season or None.
|
|
84
|
+
"""
|
|
85
|
+
seasons = await self.list()
|
|
86
|
+
for season in seasons:
|
|
87
|
+
if season.from_year == year:
|
|
88
|
+
return season
|
|
89
|
+
return None
|
|
90
|
+
|
|
91
|
+
async def get_season_id_by_year(self, year: int) -> int | None:
|
|
92
|
+
"""Get season_id by year.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
year: The year to search for.
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
season_id or None if not found.
|
|
99
|
+
"""
|
|
100
|
+
season = await self.get_by_year(year)
|
|
101
|
+
return season.season_id if season else None
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Structures API endpoints."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
from uuid import UUID
|
|
7
|
+
|
|
8
|
+
from pydantic import BaseModel
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from .client import AssistAgroClient
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Structure(BaseModel):
|
|
15
|
+
model_config = {"populate_by_name": True, "extra": "ignore"}
|
|
16
|
+
|
|
17
|
+
structure_guid: UUID
|
|
18
|
+
structure_ext_id: str | None = None
|
|
19
|
+
name: str
|
|
20
|
+
company_guid: UUID | None = None
|
|
21
|
+
area_fact_hectare: float | None = None
|
|
22
|
+
superfield_count: int | None = None
|
|
23
|
+
region_id: int | None = None
|
|
24
|
+
structure_parent_guid: UUID | None = None
|
|
25
|
+
structure_parent_ext_id: str | None = None
|
|
26
|
+
inn: str | None = None
|
|
27
|
+
active_flag: bool = True
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class StructuresAPI:
|
|
31
|
+
"""Structures API endpoints."""
|
|
32
|
+
|
|
33
|
+
def __init__(self, client: AssistAgroClient):
|
|
34
|
+
self._client = client
|
|
35
|
+
|
|
36
|
+
async def list_(self) -> list[Structure]:
|
|
37
|
+
"""List structures."""
|
|
38
|
+
response = await self._client.get("/structures/list")
|
|
39
|
+
response.raise_for_status()
|
|
40
|
+
data = self._client._parse_json_response(response)
|
|
41
|
+
if isinstance(data, dict) and "structures" in data:
|
|
42
|
+
structures_data = data["structures"]
|
|
43
|
+
else:
|
|
44
|
+
structures_data = data if data else []
|
|
45
|
+
return (
|
|
46
|
+
[Structure(**item) for item in structures_data] if structures_data else []
|
|
47
|
+
)
|
|
@@ -13,17 +13,19 @@ if TYPE_CHECKING:
|
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
class Task(BaseModel):
|
|
16
|
+
model_config = {"populate_by_name": True, "extra": "ignore"}
|
|
17
|
+
|
|
16
18
|
guid: UUID
|
|
17
19
|
task_ext_id: str | None = None
|
|
18
|
-
season_id: int
|
|
19
|
-
priority_id: int
|
|
20
|
+
season_id: int | None = None
|
|
21
|
+
priority_id: int | None = None
|
|
20
22
|
entity_guid: UUID | None = None
|
|
21
23
|
entity_type: str | None = None
|
|
22
|
-
status_id: int
|
|
23
|
-
name: str
|
|
24
|
+
status_id: int | None = None
|
|
25
|
+
name: str | None = None
|
|
24
26
|
description: str | None = None
|
|
25
|
-
created_at: datetime
|
|
26
|
-
updated_at: datetime
|
|
27
|
+
created_at: datetime | None = None
|
|
28
|
+
updated_at: datetime | None = None
|
|
27
29
|
|
|
28
30
|
|
|
29
31
|
class TaskCreateRequest(BaseModel):
|
|
@@ -67,27 +69,49 @@ class TasksAPI:
|
|
|
67
69
|
},
|
|
68
70
|
)
|
|
69
71
|
response.raise_for_status()
|
|
70
|
-
|
|
72
|
+
data = self._client._parse_json_response(response)
|
|
73
|
+
return Task(**data) if data else None
|
|
71
74
|
|
|
72
75
|
async def list_(
|
|
73
76
|
self,
|
|
77
|
+
structure_guid: UUID | None = None,
|
|
78
|
+
superfield_guid: UUID | None = None,
|
|
74
79
|
limit: int = 100,
|
|
75
80
|
offset: int = 0,
|
|
76
81
|
season_id: int | None = None,
|
|
77
82
|
status_id: int | None = None,
|
|
78
83
|
) -> list[Task]:
|
|
79
|
-
"""List tasks with filters.
|
|
84
|
+
"""List tasks with filters.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
structure_guid: Filter by structure GUID.
|
|
88
|
+
superfield_guid: Filter by superfield GUID.
|
|
89
|
+
limit: Maximum number of results.
|
|
90
|
+
offset: Offset for pagination.
|
|
91
|
+
season_id: Filter by season ID.
|
|
92
|
+
status_id: Filter by status ID.
|
|
93
|
+
"""
|
|
80
94
|
params = {"limit": limit, "offset": offset}
|
|
95
|
+
if structure_guid:
|
|
96
|
+
params["structure_guid"] = str(structure_guid)
|
|
97
|
+
if superfield_guid:
|
|
98
|
+
params["superfield_guid"] = str(superfield_guid)
|
|
81
99
|
if season_id:
|
|
82
100
|
params["season_id"] = season_id
|
|
83
101
|
if status_id:
|
|
84
102
|
params["status_id"] = status_id
|
|
85
103
|
response = await self._client.get("/tasks/list", params=params)
|
|
86
104
|
response.raise_for_status()
|
|
87
|
-
|
|
105
|
+
data = self._client._parse_json_response(response)
|
|
106
|
+
if isinstance(data, list):
|
|
107
|
+
return (
|
|
108
|
+
[Task(**item) for item in data if isinstance(item, dict)]
|
|
109
|
+
if data
|
|
110
|
+
else []
|
|
111
|
+
)
|
|
112
|
+
return []
|
|
88
113
|
|
|
89
114
|
async def get_statuses(self) -> list[dict]:
|
|
90
115
|
"""Get available task statuses."""
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
return response.json()
|
|
116
|
+
# Endpoint might not exist - return empty list
|
|
117
|
+
return []
|
|
@@ -13,22 +13,26 @@ if TYPE_CHECKING:
|
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
class TechmapOperation(BaseModel):
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
16
|
+
model_config = {"populate_by_name": True, "extra": "ignore"}
|
|
17
|
+
|
|
18
|
+
techoperation_guid: UUID | None = None
|
|
19
|
+
begin_date: date | None = None
|
|
20
|
+
end_date: date | None = None
|
|
21
|
+
name: str | None = None
|
|
20
22
|
|
|
21
23
|
|
|
22
24
|
class Techmap(BaseModel):
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
25
|
+
model_config = {"populate_by_name": True, "extra": "ignore"}
|
|
26
|
+
|
|
27
|
+
guid: UUID | None = None
|
|
28
|
+
season_id: int | None = None
|
|
29
|
+
crop_id: int | None = None
|
|
30
|
+
structure_guid: UUID | None = None
|
|
31
|
+
name: str | None = None
|
|
28
32
|
technology: str | None = None
|
|
29
|
-
techoperations: list[TechmapOperation]
|
|
30
|
-
created_at: datetime
|
|
31
|
-
updated_at: datetime
|
|
33
|
+
techoperations: list[TechmapOperation] | None = None
|
|
34
|
+
created_at: datetime | None = None
|
|
35
|
+
updated_at: datetime | None = None
|
|
32
36
|
|
|
33
37
|
|
|
34
38
|
class TechmapCreateRequest(BaseModel):
|
|
@@ -70,14 +74,31 @@ class TechmapsAPI:
|
|
|
70
74
|
response.raise_for_status()
|
|
71
75
|
return Techmap(**response.json())
|
|
72
76
|
|
|
73
|
-
async def list_(
|
|
74
|
-
|
|
75
|
-
|
|
77
|
+
async def list_(
|
|
78
|
+
self,
|
|
79
|
+
structure_guids: list[UUID],
|
|
80
|
+
season_id: int,
|
|
81
|
+
) -> list[Techmap]:
|
|
82
|
+
"""List techmaps for given structure and season.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
structure_guids: List of structure GUIDs (required, must have at least 1).
|
|
86
|
+
season_id: Season ID (required).
|
|
87
|
+
"""
|
|
88
|
+
response = await self._client.get(
|
|
89
|
+
"/techmaps/list",
|
|
90
|
+
params={
|
|
91
|
+
"structure_guids": ",".join(str(g) for g in structure_guids),
|
|
92
|
+
"season_id": season_id,
|
|
93
|
+
},
|
|
94
|
+
)
|
|
76
95
|
response.raise_for_status()
|
|
77
|
-
|
|
96
|
+
data = self._client._parse_json_response(response)
|
|
97
|
+
return [Techmap(**item) for item in data] if data else []
|
|
78
98
|
|
|
79
99
|
async def get(self, techmap_guid: UUID) -> Techmap:
|
|
80
100
|
"""Get techmap by GUID."""
|
|
81
101
|
response = await self._client.get(f"/techmaps/{techmap_guid}")
|
|
82
102
|
response.raise_for_status()
|
|
83
|
-
|
|
103
|
+
data = self._client._parse_json_response(response)
|
|
104
|
+
return Techmap(**data) if data else None
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import logging
|
|
5
6
|
import httpx
|
|
6
7
|
from typing import Any, TYPE_CHECKING
|
|
7
8
|
|
|
@@ -9,6 +10,8 @@ from .config import settings
|
|
|
9
10
|
from .exceptions import APIError, TimeoutError, AssistAgroError
|
|
10
11
|
from .auth import Auth, AuthTokens
|
|
11
12
|
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
12
15
|
if TYPE_CHECKING:
|
|
13
16
|
from .api.v1.accounts import AccountsAPI
|
|
14
17
|
from .api.v1.companies import CompaniesAPI
|
|
@@ -19,6 +22,7 @@ if TYPE_CHECKING:
|
|
|
19
22
|
from .api.v1.dictionaries import DictionariesAPI
|
|
20
23
|
from .api.v1.meteostations import MeteostationsAPI
|
|
21
24
|
from .api.v1.structures import StructuresAPI
|
|
25
|
+
from .api.v1.seasons import SeasonsAPI
|
|
22
26
|
|
|
23
27
|
|
|
24
28
|
class AssistAgroClient:
|
|
@@ -29,10 +33,12 @@ class AssistAgroClient:
|
|
|
29
33
|
base_url: str | None = None,
|
|
30
34
|
timeout: int | None = None,
|
|
31
35
|
token: str | None = None,
|
|
36
|
+
debug: bool | None = None,
|
|
32
37
|
):
|
|
33
38
|
self.base_url = base_url or settings.base_url
|
|
34
39
|
self.timeout = timeout or settings.timeout
|
|
35
40
|
self._token = token
|
|
41
|
+
self._debug = debug if debug is not None else settings.debug
|
|
36
42
|
self._user_data: dict[str, Any] = {}
|
|
37
43
|
self._permissions: list[str] = []
|
|
38
44
|
self._client: httpx.AsyncClient | None = None
|
|
@@ -47,6 +53,7 @@ class AssistAgroClient:
|
|
|
47
53
|
self._dictionaries: DictionariesAPI | None = None
|
|
48
54
|
self._meteostations: MeteostationsAPI | None = None
|
|
49
55
|
self._structures: StructuresAPI | None = None
|
|
56
|
+
self._seasons: SeasonsAPI | None = None
|
|
50
57
|
|
|
51
58
|
@property
|
|
52
59
|
def token(self) -> str | None:
|
|
@@ -144,6 +151,14 @@ class AssistAgroClient:
|
|
|
144
151
|
self._structures = StructuresAPI(self)
|
|
145
152
|
return self._structures
|
|
146
153
|
|
|
154
|
+
@property
|
|
155
|
+
def seasons(self) -> SeasonsAPI:
|
|
156
|
+
if self._seasons is None:
|
|
157
|
+
from .api.v1.seasons import SeasonsAPI
|
|
158
|
+
|
|
159
|
+
self._seasons = SeasonsAPI(self)
|
|
160
|
+
return self._seasons
|
|
161
|
+
|
|
147
162
|
def _get_headers(self) -> dict[str, str]:
|
|
148
163
|
headers: dict[str, str] = {
|
|
149
164
|
"Content-Type": "application/json",
|
|
@@ -183,14 +198,27 @@ class AssistAgroClient:
|
|
|
183
198
|
) -> httpx.Response:
|
|
184
199
|
"""Make HTTP request."""
|
|
185
200
|
client = await self._get_client()
|
|
201
|
+
|
|
202
|
+
# Merge custom headers with auth headers
|
|
203
|
+
request_headers = self._get_headers()
|
|
204
|
+
if "headers" in kwargs:
|
|
205
|
+
request_headers.update(kwargs.pop("headers"))
|
|
206
|
+
|
|
186
207
|
try:
|
|
208
|
+
if self._debug:
|
|
209
|
+
logger.debug(f"Request: {method} {path}")
|
|
187
210
|
response = await client.request(
|
|
188
211
|
method=method,
|
|
189
212
|
url=path,
|
|
190
|
-
headers=
|
|
213
|
+
headers=request_headers,
|
|
191
214
|
**kwargs,
|
|
192
215
|
)
|
|
216
|
+
if self._debug:
|
|
217
|
+
logger.debug(
|
|
218
|
+
f"Response: {response.status_code} {response.text[:200] if response.text else 'empty'}"
|
|
219
|
+
)
|
|
193
220
|
if response.status_code >= 400:
|
|
221
|
+
logger.warning(f"API Error {response.status_code}: {response.text}")
|
|
194
222
|
raise APIError(
|
|
195
223
|
message=response.text or response.reason_phrase,
|
|
196
224
|
status_code=response.status_code,
|
|
@@ -201,6 +229,12 @@ class AssistAgroClient:
|
|
|
201
229
|
except httpx.HTTPError as e:
|
|
202
230
|
raise AssistAgroError(f"HTTP error: {e}") from e
|
|
203
231
|
|
|
232
|
+
def _parse_json_response(self, response: httpx.Response) -> Any:
|
|
233
|
+
"""Parse JSON response, handling empty responses."""
|
|
234
|
+
if not response.content:
|
|
235
|
+
return []
|
|
236
|
+
return response.json()
|
|
237
|
+
|
|
204
238
|
async def get(self, path: str, **kwargs: Any) -> httpx.Response:
|
|
205
239
|
"""GET request."""
|
|
206
240
|
return await self.request("GET", path, **kwargs)
|
|
@@ -6,7 +6,7 @@ from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
|
6
6
|
class Settings(BaseSettings):
|
|
7
7
|
model_config = SettingsConfigDict(env_prefix="ASSISTAGRO_")
|
|
8
8
|
|
|
9
|
-
base_url: str = "https://
|
|
9
|
+
base_url: str = "https://gateway-frontend.agroassist.ru"
|
|
10
10
|
timeout: int = 30
|
|
11
11
|
debug: bool = False
|
|
12
12
|
|
|
File without changes
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
"""Companies API endpoints."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
from datetime import date
|
|
6
|
-
from typing import TYPE_CHECKING
|
|
7
|
-
from uuid import UUID
|
|
8
|
-
|
|
9
|
-
from pydantic import BaseModel
|
|
10
|
-
|
|
11
|
-
if TYPE_CHECKING:
|
|
12
|
-
from .client import AssistAgroClient
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
class License(BaseModel):
|
|
16
|
-
begin_date: date
|
|
17
|
-
end_date: date
|
|
18
|
-
modules: list[int]
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
class Company(BaseModel):
|
|
22
|
-
guid: UUID
|
|
23
|
-
name: str
|
|
24
|
-
user_count: int
|
|
25
|
-
active_flag: bool
|
|
26
|
-
license: License
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
class CompaniesAPI:
|
|
30
|
-
"""Companies API endpoints."""
|
|
31
|
-
|
|
32
|
-
def __init__(self, client: AssistAgroClient):
|
|
33
|
-
self._client = client
|
|
34
|
-
|
|
35
|
-
async def list_(self) -> list[Company]:
|
|
36
|
-
"""Get all companies."""
|
|
37
|
-
response = await self._client.get("/companies")
|
|
38
|
-
response.raise_for_status()
|
|
39
|
-
return [Company(**item) for item in response.json()]
|
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
"""Fields API endpoints."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
from datetime import datetime
|
|
6
|
-
from typing import TYPE_CHECKING
|
|
7
|
-
from uuid import UUID
|
|
8
|
-
|
|
9
|
-
from pydantic import BaseModel
|
|
10
|
-
|
|
11
|
-
if TYPE_CHECKING:
|
|
12
|
-
from .client import AssistAgroClient
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
class FieldContour(BaseModel):
|
|
16
|
-
contour_guid: UUID
|
|
17
|
-
field_guid: UUID
|
|
18
|
-
superfield_guid: UUID
|
|
19
|
-
contour: str
|
|
20
|
-
area_fact_hectare: float
|
|
21
|
-
area_etalon_hectare: float
|
|
22
|
-
start_datetime: datetime
|
|
23
|
-
author_guid: UUID
|
|
24
|
-
editor_guid: UUID
|
|
25
|
-
created_at: datetime
|
|
26
|
-
updated_at: datetime
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
class FieldListItem(BaseModel):
|
|
30
|
-
guid: UUID
|
|
31
|
-
name: str
|
|
32
|
-
company_guid: UUID
|
|
33
|
-
area_fact_hectare: float
|
|
34
|
-
area_etalon_hectare: float
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
class FieldsAPI:
|
|
38
|
-
"""Fields API endpoints."""
|
|
39
|
-
|
|
40
|
-
def __init__(self, client: AssistAgroClient):
|
|
41
|
-
self._client = client
|
|
42
|
-
|
|
43
|
-
async def get_contours(self, field_guid: UUID) -> list[FieldContour]:
|
|
44
|
-
"""Get field contours."""
|
|
45
|
-
response = await self._client.get(
|
|
46
|
-
"/fields/contours",
|
|
47
|
-
params={"field_guid": str(field_guid)},
|
|
48
|
-
)
|
|
49
|
-
response.raise_for_status()
|
|
50
|
-
return [FieldContour(**item) for item in response.json()]
|
|
51
|
-
|
|
52
|
-
async def list_(self) -> list[FieldListItem]:
|
|
53
|
-
"""List all fields."""
|
|
54
|
-
response = await self._client.get("/fields/list")
|
|
55
|
-
response.raise_for_status()
|
|
56
|
-
return [FieldListItem(**item) for item in response.json()]
|
|
57
|
-
|
|
58
|
-
async def list_meta(self) -> dict:
|
|
59
|
-
"""List fields with metadata."""
|
|
60
|
-
response = await self._client.get("/fields/list_meta")
|
|
61
|
-
response.raise_for_status()
|
|
62
|
-
return response.json()
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
"""Structures API endpoints."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
from typing import TYPE_CHECKING
|
|
6
|
-
from uuid import UUID
|
|
7
|
-
|
|
8
|
-
from pydantic import BaseModel
|
|
9
|
-
|
|
10
|
-
if TYPE_CHECKING:
|
|
11
|
-
from .client import AssistAgroClient
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
class Structure(BaseModel):
|
|
15
|
-
guid: UUID
|
|
16
|
-
name: str
|
|
17
|
-
company_guid: UUID
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
class StructuresAPI:
|
|
21
|
-
"""Structures API endpoints."""
|
|
22
|
-
|
|
23
|
-
def __init__(self, client: AssistAgroClient):
|
|
24
|
-
self._client = client
|
|
25
|
-
|
|
26
|
-
async def list_(self) -> list[Structure]:
|
|
27
|
-
"""List structures."""
|
|
28
|
-
response = await self._client.get("/structures")
|
|
29
|
-
response.raise_for_status()
|
|
30
|
-
return [Structure(**item) for item in response.json()]
|
|
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
|