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.
Files changed (33) hide show
  1. {assistagroapi-0.1.3 → assistagroapi-0.2.4}/PKG-INFO +3 -2
  2. {assistagroapi-0.1.3 → assistagroapi-0.2.4}/pyproject.toml +3 -2
  3. {assistagroapi-0.1.3 → assistagroapi-0.2.4}/src/AssistagroAPI.egg-info/PKG-INFO +3 -2
  4. {assistagroapi-0.1.3 → assistagroapi-0.2.4}/src/AssistagroAPI.egg-info/SOURCES.txt +2 -0
  5. {assistagroapi-0.1.3 → assistagroapi-0.2.4}/src/AssistagroAPI.egg-info/requires.txt +1 -0
  6. {assistagroapi-0.1.3 → assistagroapi-0.2.4}/src/assistagro_client/__init__.py +2 -0
  7. {assistagroapi-0.1.3 → assistagroapi-0.2.4}/src/assistagro_client/api/v1/accounts.py +9 -4
  8. assistagroapi-0.2.4/src/assistagro_client/api/v1/companies.py +50 -0
  9. {assistagroapi-0.1.3 → assistagroapi-0.2.4}/src/assistagro_client/api/v1/dictionaries.py +11 -12
  10. assistagroapi-0.2.4/src/assistagro_client/api/v1/fields.py +102 -0
  11. {assistagroapi-0.1.3 → assistagroapi-0.2.4}/src/assistagro_client/api/v1/meteostations.py +6 -3
  12. {assistagroapi-0.1.3 → assistagroapi-0.2.4}/src/assistagro_client/api/v1/reports.py +2 -1
  13. assistagroapi-0.2.4/src/assistagro_client/api/v1/seasons.py +101 -0
  14. assistagroapi-0.2.4/src/assistagro_client/api/v1/structures.py +47 -0
  15. {assistagroapi-0.1.3 → assistagroapi-0.2.4}/src/assistagro_client/api/v1/tasks.py +36 -12
  16. {assistagroapi-0.1.3 → assistagroapi-0.2.4}/src/assistagro_client/api/v1/techmaps.py +38 -17
  17. {assistagroapi-0.1.3 → assistagroapi-0.2.4}/src/assistagro_client/client.py +35 -1
  18. {assistagroapi-0.1.3 → assistagroapi-0.2.4}/src/assistagro_client/config.py +1 -1
  19. assistagroapi-0.2.4/src/assistagro_client/py.typed +0 -0
  20. assistagroapi-0.1.3/src/assistagro_client/api/v1/companies.py +0 -39
  21. assistagroapi-0.1.3/src/assistagro_client/api/v1/fields.py +0 -62
  22. assistagroapi-0.1.3/src/assistagro_client/api/v1/structures.py +0 -30
  23. {assistagroapi-0.1.3 → assistagroapi-0.2.4}/README.md +0 -0
  24. {assistagroapi-0.1.3 → assistagroapi-0.2.4}/setup.cfg +0 -0
  25. {assistagroapi-0.1.3 → assistagroapi-0.2.4}/src/AssistagroAPI.egg-info/dependency_links.txt +0 -0
  26. {assistagroapi-0.1.3 → assistagroapi-0.2.4}/src/AssistagroAPI.egg-info/top_level.txt +0 -0
  27. {assistagroapi-0.1.3 → assistagroapi-0.2.4}/src/assistagro_client/api/__init__.py +0 -0
  28. {assistagroapi-0.1.3 → assistagroapi-0.2.4}/src/assistagro_client/api/v1/__init__.py +0 -0
  29. {assistagroapi-0.1.3 → assistagroapi-0.2.4}/src/assistagro_client/auth.py +0 -0
  30. {assistagroapi-0.1.3 → assistagroapi-0.2.4}/src/assistagro_client/exceptions.py +0 -0
  31. {assistagroapi-0.1.3 → assistagroapi-0.2.4}/src/assistagro_client/models/__init__.py +0 -0
  32. {assistagroapi-0.1.3 → assistagroapi-0.2.4}/tests/test_auth.py +0 -0
  33. {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.1.3
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.8
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.1.3"
3
+ version = "0.2.4"
4
4
  description = "Async HTTP client for AssistAgro API"
5
5
  readme = "README.md"
6
- requires-python = ">=3.8"
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.1.3
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.8
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
@@ -2,3 +2,4 @@ httpx>=0.28.1
2
2
  pydantic>=2.12.5
3
3
  pydantic-settings>=2.13.1
4
4
  python-dateutil>=2.9.0.post0
5
+ python-dotenv>=1.0.0
@@ -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: list[str]
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
- return [UserProfile(**item) for item in response.json()]
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
- return response.json()
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
- return response.json()
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 response.json()
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 response.json()
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
- response = await self._client.get("/dictionaries/techoperations")
32
- response.raise_for_status()
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 response.json()
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 response.json()
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 response.json()
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 response.json()
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 response.json()
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 response.json()
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
- return [Meteostation(**item) for item in response.json()]
33
+ data = self._client._parse_json_response(response)
34
+ return [Meteostation(**item) for item in data] if data else []
@@ -64,4 +64,5 @@ class ReportsAPI:
64
64
  },
65
65
  )
66
66
  response.raise_for_status()
67
- return Report(**response.json())
67
+ data = self._client._parse_json_response(response)
68
+ return Report(**data) if data else None
@@ -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
- return Task(**response.json())
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
- return [Task(**item) for item in response.json()]
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
- response = await self._client.get("/tasks/status")
92
- response.raise_for_status()
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
- techoperation_guid: UUID
17
- begin_date: date
18
- end_date: date
19
- name: str
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
- guid: UUID
24
- season_id: int
25
- crop_id: int
26
- structure_guid: UUID
27
- name: str
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_(self) -> list[Techmap]:
74
- """List techmaps."""
75
- response = await self._client.get("/techmaps/list")
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
- return [Techmap(**item) for item in response.json()]
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
- return Techmap(**response.json())
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=self._get_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://dev-gateway-frontend.agroassist.ru"
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