AssistagroAPI 0.1.0__py3-none-any.whl

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.
@@ -0,0 +1,17 @@
1
+ """AssistAgro API Client."""
2
+
3
+ from .client import AssistAgroClient
4
+ from .auth import Auth, AuthTokens, SignInRequest, OTPResponse
5
+ from .exceptions import AssistAgroError, AuthenticationError, APIError, TimeoutError
6
+
7
+ __all__ = [
8
+ "AssistAgroClient",
9
+ "Auth",
10
+ "AuthTokens",
11
+ "SignInRequest",
12
+ "OTPResponse",
13
+ "AssistAgroError",
14
+ "AuthenticationError",
15
+ "APIError",
16
+ "TimeoutError",
17
+ ]
@@ -0,0 +1 @@
1
+ """API v1 endpoints."""
@@ -0,0 +1 @@
1
+ """API v1 endpoints."""
@@ -0,0 +1,53 @@
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 pydantic import BaseModel
9
+
10
+ if TYPE_CHECKING:
11
+ from .client import AssistAgroClient
12
+
13
+
14
+ class Company(BaseModel):
15
+ guid: UUID
16
+ name: str
17
+ parent_guid: UUID | None = None
18
+
19
+
20
+ class UserProfile(BaseModel):
21
+ company: Company
22
+ user_guid: UUID
23
+ permissions: list[str]
24
+ is_current: bool
25
+ is_confirmed: bool
26
+
27
+
28
+ class AccountsAPI:
29
+ """Accounts API endpoints."""
30
+
31
+ def __init__(self, client: AssistAgroClient):
32
+ self._client = client
33
+
34
+ async def get_users(self, account_guid: UUID | None = None) -> list[UserProfile]:
35
+ """Get user profiles."""
36
+ params = {}
37
+ if account_guid:
38
+ params["account_guid"] = str(account_guid)
39
+ response = await self._client.get("/account/users", params=params)
40
+ response.raise_for_status()
41
+ return [UserProfile(**item) for item in response.json()]
42
+
43
+ async def get_current_user(self) -> dict:
44
+ """Get current user info."""
45
+ response = await self._client.get("/account/users/current")
46
+ response.raise_for_status()
47
+ return response.json()
48
+
49
+ async def get_user(self, user_guid: UUID) -> dict:
50
+ """Get user by GUID."""
51
+ response = await self._client.get(f"/account/users/{user_guid}")
52
+ response.raise_for_status()
53
+ return response.json()
@@ -0,0 +1,39 @@
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()]
@@ -0,0 +1,69 @@
1
+ """Dictionaries API endpoints."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING, Any
6
+
7
+ if TYPE_CHECKING:
8
+ from .client import AssistAgroClient
9
+
10
+
11
+ class DictionariesAPI:
12
+ """Dictionaries API endpoints."""
13
+
14
+ def __init__(self, client: AssistAgroClient):
15
+ self._client = client
16
+
17
+ async def get_crops(self) -> list[dict[str, Any]]:
18
+ """Get crops dictionary."""
19
+ response = await self._client.get("/dictionaries/crops")
20
+ response.raise_for_status()
21
+ return response.json()
22
+
23
+ async def get_crop_products(self) -> list[dict[str, Any]]:
24
+ """Get crop products dictionary."""
25
+ response = await self._client.get("/dictionaries/crop_products")
26
+ response.raise_for_status()
27
+ return response.json()
28
+
29
+ async def get_techoperations(self) -> list[dict[str, Any]]:
30
+ """Get techoperations dictionary."""
31
+ response = await self._client.get("/dictionaries/techoperations")
32
+ response.raise_for_status()
33
+ return response.json()
34
+
35
+ async def get_machine_models(self) -> list[dict[str, Any]]:
36
+ """Get machine models dictionary."""
37
+ response = await self._client.get("/dictionaries/machine_models")
38
+ response.raise_for_status()
39
+ return response.json()
40
+
41
+ async def get_pesticides(self) -> list[dict[str, Any]]:
42
+ """Get pesticides dictionary."""
43
+ response = await self._client.get("/dictionaries/pesticides")
44
+ response.raise_for_status()
45
+ return response.json()
46
+
47
+ async def get_fertilizers(self) -> list[dict[str, Any]]:
48
+ """Get fertilizers dictionary."""
49
+ response = await self._client.get("/dictionaries/fertilizers")
50
+ response.raise_for_status()
51
+ return response.json()
52
+
53
+ async def get_varieties(self) -> list[dict[str, Any]]:
54
+ """Get varieties dictionary."""
55
+ response = await self._client.get("/dictionaries/varieties")
56
+ response.raise_for_status()
57
+ return response.json()
58
+
59
+ async def get_meteostations(self) -> list[dict[str, Any]]:
60
+ """Get meteostations."""
61
+ response = await self._client.get("/meteostations")
62
+ response.raise_for_status()
63
+ return response.json()
64
+
65
+ async def get_notifications_channels(self) -> list[dict[str, Any]]:
66
+ """Get notification channels."""
67
+ response = await self._client.get("/notifications/channels")
68
+ response.raise_for_status()
69
+ return response.json()
@@ -0,0 +1,62 @@
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()
@@ -0,0 +1,31 @@
1
+ """Meteostations 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 Meteostation(BaseModel):
15
+ guid: UUID
16
+ name: str
17
+ latitude: float
18
+ longitude: float
19
+
20
+
21
+ class MeteostationsAPI:
22
+ """Meteostations API endpoints."""
23
+
24
+ def __init__(self, client: AssistAgroClient):
25
+ self._client = client
26
+
27
+ async def list_(self) -> list[Meteostation]:
28
+ """List all meteostations."""
29
+ response = await self._client.get("/meteostations")
30
+ response.raise_for_status()
31
+ return [Meteostation(**item) for item in response.json()]
@@ -0,0 +1,67 @@
1
+ """Reports 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 Report(BaseModel):
16
+ guid: UUID
17
+ task_guid: UUID | None = None
18
+ season_id: int
19
+ entity_guid: UUID | None = None
20
+ entity_type: str | None = None
21
+ status_id: int
22
+ name: str
23
+ created_at: datetime
24
+ updated_at: datetime
25
+
26
+
27
+ class ReportCreateRequest(BaseModel):
28
+ task_guid: UUID | None = None
29
+ season_id: int
30
+ entity_guid: UUID | None = None
31
+ entity_type: str | None = None
32
+ entity_data: dict[str, Any]
33
+ datetimes: dict
34
+ history: list[dict]
35
+
36
+
37
+ class ReportsAPI:
38
+ """Reports API endpoints."""
39
+
40
+ def __init__(self, client: AssistAgroClient):
41
+ self._client = client
42
+
43
+ async def create(
44
+ self,
45
+ season_id: int,
46
+ entity_data: dict[str, Any],
47
+ datetimes: dict,
48
+ history: list[dict],
49
+ task_guid: UUID | None = None,
50
+ entity_guid: UUID | None = None,
51
+ entity_type: str | None = None,
52
+ ) -> Report:
53
+ """Create a report."""
54
+ response = await self._client.post(
55
+ "/reports",
56
+ json={
57
+ "task_guid": str(task_guid) if task_guid else None,
58
+ "season_id": season_id,
59
+ "entity_guid": str(entity_guid) if entity_guid else None,
60
+ "entity_type": entity_type,
61
+ "entity_data": entity_data,
62
+ "datetimes": datetimes,
63
+ "history": history,
64
+ },
65
+ )
66
+ response.raise_for_status()
67
+ return Report(**response.json())
@@ -0,0 +1,30 @@
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()]
@@ -0,0 +1,93 @@
1
+ """Tasks 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 Task(BaseModel):
16
+ guid: UUID
17
+ task_ext_id: str | None = None
18
+ season_id: int
19
+ priority_id: int
20
+ entity_guid: UUID | None = None
21
+ entity_type: str | None = None
22
+ status_id: int
23
+ name: str
24
+ description: str | None = None
25
+ created_at: datetime
26
+ updated_at: datetime
27
+
28
+
29
+ class TaskCreateRequest(BaseModel):
30
+ season_id: int
31
+ priority_id: int
32
+ entity_data: dict
33
+ datetimes: dict
34
+ history: list[dict]
35
+
36
+
37
+ class TasksAPI:
38
+ """Tasks API endpoints."""
39
+
40
+ def __init__(self, client: AssistAgroClient):
41
+ self._client = client
42
+
43
+ async def create(
44
+ self,
45
+ season_id: int,
46
+ priority_id: int,
47
+ entity_data: dict,
48
+ datetimes: dict,
49
+ history: list[dict],
50
+ task_ext_id: str | None = None,
51
+ check_deleted_objects: bool = True,
52
+ ) -> Task:
53
+ """Create a task."""
54
+ params = {}
55
+ if not check_deleted_objects:
56
+ params["check_deleted_objects"] = "false"
57
+ response = await self._client.post(
58
+ "/tasks",
59
+ params=params,
60
+ json={
61
+ "season_id": season_id,
62
+ "priority_id": priority_id,
63
+ "entity_data": entity_data,
64
+ "datetimes": datetimes,
65
+ "history": history,
66
+ "task_ext_id": task_ext_id,
67
+ },
68
+ )
69
+ response.raise_for_status()
70
+ return Task(**response.json())
71
+
72
+ async def list_(
73
+ self,
74
+ limit: int = 100,
75
+ offset: int = 0,
76
+ season_id: int | None = None,
77
+ status_id: int | None = None,
78
+ ) -> list[Task]:
79
+ """List tasks with filters."""
80
+ params = {"limit": limit, "offset": offset}
81
+ if season_id:
82
+ params["season_id"] = season_id
83
+ if status_id:
84
+ params["status_id"] = status_id
85
+ response = await self._client.get("/tasks/list", params=params)
86
+ response.raise_for_status()
87
+ return [Task(**item) for item in response.json()]
88
+
89
+ async def get_statuses(self) -> list[dict]:
90
+ """Get available task statuses."""
91
+ response = await self._client.get("/tasks/status")
92
+ response.raise_for_status()
93
+ return response.json()
@@ -0,0 +1,83 @@
1
+ """Techmaps API endpoints."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from datetime import date, 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 TechmapOperation(BaseModel):
16
+ techoperation_guid: UUID
17
+ begin_date: date
18
+ end_date: date
19
+ name: str
20
+
21
+
22
+ class Techmap(BaseModel):
23
+ guid: UUID
24
+ season_id: int
25
+ crop_id: int
26
+ structure_guid: UUID
27
+ name: str
28
+ technology: str | None = None
29
+ techoperations: list[TechmapOperation]
30
+ created_at: datetime
31
+ updated_at: datetime
32
+
33
+
34
+ class TechmapCreateRequest(BaseModel):
35
+ season_id: int
36
+ crop_id: int
37
+ structure_guid: UUID
38
+ name: str
39
+ technology: str | None = None
40
+ techoperations: list[dict]
41
+
42
+
43
+ class TechmapsAPI:
44
+ """Techmaps API endpoints."""
45
+
46
+ def __init__(self, client: AssistAgroClient):
47
+ self._client = client
48
+
49
+ async def create(
50
+ self,
51
+ season_id: int,
52
+ crop_id: int,
53
+ structure_guid: UUID,
54
+ name: str,
55
+ techoperations: list[dict],
56
+ technology: str | None = None,
57
+ ) -> Techmap:
58
+ """Create a techmap."""
59
+ response = await self._client.post(
60
+ "/techmaps",
61
+ json={
62
+ "season_id": season_id,
63
+ "crop_id": crop_id,
64
+ "structure_guid": str(structure_guid),
65
+ "name": name,
66
+ "technology": technology,
67
+ "techoperations": techoperations,
68
+ },
69
+ )
70
+ response.raise_for_status()
71
+ return Techmap(**response.json())
72
+
73
+ async def list_(self) -> list[Techmap]:
74
+ """List techmaps."""
75
+ response = await self._client.get("/techmaps/list")
76
+ response.raise_for_status()
77
+ return [Techmap(**item) for item in response.json()]
78
+
79
+ async def get(self, techmap_guid: UUID) -> Techmap:
80
+ """Get techmap by GUID."""
81
+ response = await self._client.get(f"/techmaps/{techmap_guid}")
82
+ response.raise_for_status()
83
+ return Techmap(**response.json())
@@ -0,0 +1,100 @@
1
+ """Authentication module."""
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
+ from .exceptions import AuthenticationError
14
+
15
+
16
+ class SignInRequest(BaseModel):
17
+ email: str = Field(..., min_length=7, max_length=255)
18
+ password: str = Field(..., min_length=12, max_length=255)
19
+
20
+
21
+ class OTPResponse(BaseModel):
22
+ session_guid: str
23
+ attempts: int
24
+ max_attempts: int
25
+ expires_at: datetime
26
+ next_request_available_at: datetime
27
+ code_request: int
28
+ max_code_request: int
29
+
30
+
31
+ class AuthTokens(BaseModel):
32
+ access_token: str
33
+ access_token_expires_in: float
34
+ refresh_token: str
35
+ refresh_token_expires_in: float
36
+ superset_token: str | None = None
37
+ superset_token_expires_in: float | None = None
38
+
39
+
40
+ class RefreshTokensRequest(BaseModel):
41
+ refresh_token: str
42
+
43
+
44
+ class Auth:
45
+ """Authentication handler."""
46
+
47
+ def __init__(self, client: AssistAgroClient):
48
+ self._client = client
49
+
50
+ async def sign_in(self, email: str, password: str) -> AuthTokens | OTPResponse:
51
+ """Sign in with email and password.
52
+
53
+ Returns AuthTokens if 2FA is disabled, OTPResponse if enabled.
54
+ """
55
+ response = await self._client.post(
56
+ "/account/sign_in",
57
+ json={"email": email, "password": password},
58
+ )
59
+ if response.status_code == 401:
60
+ raise AuthenticationError("Invalid email or password")
61
+ response.raise_for_status()
62
+ data = response.json()
63
+ if "session_guid" in data:
64
+ return OTPResponse(**data)
65
+ tokens = AuthTokens(**data)
66
+ self._client.set_auth_tokens(tokens)
67
+ return tokens
68
+
69
+ async def confirm_otp(self, session_guid: str, code: str) -> AuthTokens:
70
+ """Confirm OTP code."""
71
+ response = await self._client.post(
72
+ "/account/otp/validation",
73
+ json={"session_guid": session_guid, "code": code},
74
+ )
75
+ if response.status_code == 401:
76
+ raise AuthenticationError("Invalid OTP code")
77
+ response.raise_for_status()
78
+ tokens = AuthTokens(**response.json())
79
+ self._client.set_auth_tokens(tokens)
80
+ return tokens
81
+
82
+ async def refresh_tokens(self, refresh_token: str) -> AuthTokens:
83
+ """Refresh access token."""
84
+ response = await self._client.post(
85
+ "/account/refresh_tokens",
86
+ json={"refresh_token": refresh_token},
87
+ )
88
+ if response.status_code == 401:
89
+ raise AuthenticationError("Invalid refresh token")
90
+ response.raise_for_status()
91
+ tokens = AuthTokens(**response.json())
92
+ self._client.set_auth_tokens(tokens)
93
+ return tokens
94
+
95
+ async def logout(self) -> None:
96
+ """Log out and invalidate tokens."""
97
+ await self._client.post("/account/logout")
98
+ self._client.token = None
99
+ self._client.set_user_data({})
100
+ self._client.set_permissions([])
@@ -0,0 +1,218 @@
1
+ """Main async client for AssistAgro API."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import httpx
6
+ from typing import Any, TYPE_CHECKING
7
+
8
+ from .config import settings
9
+ from .exceptions import APIError, TimeoutError, AssistAgroError
10
+ from .auth import Auth, AuthTokens
11
+
12
+ if TYPE_CHECKING:
13
+ from .api.v1.accounts import AccountsAPI
14
+ from .api.v1.companies import CompaniesAPI
15
+ from .api.v1.fields import FieldsAPI
16
+ from .api.v1.tasks import TasksAPI
17
+ from .api.v1.techmaps import TechmapsAPI
18
+ from .api.v1.reports import ReportsAPI
19
+ from .api.v1.dictionaries import DictionariesAPI
20
+ from .api.v1.meteostations import MeteostationsAPI
21
+ from .api.v1.structures import StructuresAPI
22
+
23
+
24
+ class AssistAgroClient:
25
+ """Async HTTP client for AssistAgro API."""
26
+
27
+ def __init__(
28
+ self,
29
+ base_url: str | None = None,
30
+ timeout: int | None = None,
31
+ token: str | None = None,
32
+ ):
33
+ self.base_url = base_url or settings.base_url
34
+ self.timeout = timeout or settings.timeout
35
+ self._token = token
36
+ self._user_data: dict[str, Any] = {}
37
+ self._permissions: list[str] = []
38
+ self._client: httpx.AsyncClient | None = None
39
+ self._auth: Auth | None = None
40
+ self._auth_tokens: AuthTokens | None = None
41
+ self._accounts: AccountsAPI | None = None
42
+ self._companies: CompaniesAPI | None = None
43
+ self._fields: FieldsAPI | None = None
44
+ self._tasks: TasksAPI | None = None
45
+ self._techmaps: TechmapsAPI | None = None
46
+ self._reports: ReportsAPI | None = None
47
+ self._dictionaries: DictionariesAPI | None = None
48
+ self._meteostations: MeteostationsAPI | None = None
49
+ self._structures: StructuresAPI | None = None
50
+
51
+ @property
52
+ def token(self) -> str | None:
53
+ return self._token
54
+
55
+ @token.setter
56
+ def token(self, value: str | None) -> None:
57
+ self._token = value
58
+
59
+ def set_user_data(self, user_data: dict[str, Any]) -> None:
60
+ self._user_data = user_data
61
+
62
+ def set_permissions(self, permissions: list[str]) -> None:
63
+ self._permissions = permissions
64
+
65
+ @property
66
+ def auth(self) -> Auth:
67
+ if self._auth is None:
68
+ self._auth = Auth(self)
69
+ return self._auth
70
+
71
+ def set_auth_tokens(self, tokens: AuthTokens) -> None:
72
+ self._auth_tokens = tokens
73
+ self._token = tokens.access_token
74
+
75
+ @property
76
+ def accounts(self) -> AccountsAPI:
77
+ if self._accounts is None:
78
+ from .api.v1.accounts import AccountsAPI
79
+
80
+ self._accounts = AccountsAPI(self)
81
+ return self._accounts
82
+
83
+ @property
84
+ def companies(self) -> CompaniesAPI:
85
+ if self._companies is None:
86
+ from .api.v1.companies import CompaniesAPI
87
+
88
+ self._companies = CompaniesAPI(self)
89
+ return self._companies
90
+
91
+ @property
92
+ def fields(self) -> FieldsAPI:
93
+ if self._fields is None:
94
+ from .api.v1.fields import FieldsAPI
95
+
96
+ self._fields = FieldsAPI(self)
97
+ return self._fields
98
+
99
+ @property
100
+ def tasks(self) -> TasksAPI:
101
+ if self._tasks is None:
102
+ from .api.v1.tasks import TasksAPI
103
+
104
+ self._tasks = TasksAPI(self)
105
+ return self._tasks
106
+
107
+ @property
108
+ def techmaps(self) -> TechmapsAPI:
109
+ if self._techmaps is None:
110
+ from .api.v1.techmaps import TechmapsAPI
111
+
112
+ self._techmaps = TechmapsAPI(self)
113
+ return self._techmaps
114
+
115
+ @property
116
+ def reports(self) -> ReportsAPI:
117
+ if self._reports is None:
118
+ from .api.v1.reports import ReportsAPI
119
+
120
+ self._reports = ReportsAPI(self)
121
+ return self._reports
122
+
123
+ @property
124
+ def dictionaries(self) -> DictionariesAPI:
125
+ if self._dictionaries is None:
126
+ from .api.v1.dictionaries import DictionariesAPI
127
+
128
+ self._dictionaries = DictionariesAPI(self)
129
+ return self._dictionaries
130
+
131
+ @property
132
+ def meteostations(self) -> MeteostationsAPI:
133
+ if self._meteostations is None:
134
+ from .api.v1.meteostations import MeteostationsAPI
135
+
136
+ self._meteostations = MeteostationsAPI(self)
137
+ return self._meteostations
138
+
139
+ @property
140
+ def structures(self) -> StructuresAPI:
141
+ if self._structures is None:
142
+ from .api.v1.structures import StructuresAPI
143
+
144
+ self._structures = StructuresAPI(self)
145
+ return self._structures
146
+
147
+ def _get_headers(self) -> dict[str, str]:
148
+ headers: dict[str, str] = {
149
+ "Content-Type": "application/json",
150
+ }
151
+ if self._token:
152
+ headers["x-token"] = self._token
153
+ if self._user_data:
154
+ headers["x-user-data"] = str(self._user_data)
155
+ if self._permissions:
156
+ headers["x-permissions"] = ",".join(self._permissions)
157
+ return headers
158
+
159
+ async def _get_client(self) -> httpx.AsyncClient:
160
+ if self._client is None:
161
+ self._client = httpx.AsyncClient(
162
+ base_url=self.base_url,
163
+ timeout=self.timeout,
164
+ )
165
+ return self._client
166
+
167
+ async def close(self) -> None:
168
+ if self._client:
169
+ await self._client.aclose()
170
+ self._client = None
171
+
172
+ async def __aenter__(self) -> "AssistAgroClient":
173
+ return self
174
+
175
+ async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
176
+ await self.close()
177
+
178
+ async def request(
179
+ self,
180
+ method: str,
181
+ path: str,
182
+ **kwargs: Any,
183
+ ) -> httpx.Response:
184
+ """Make HTTP request."""
185
+ client = await self._get_client()
186
+ try:
187
+ response = await client.request(
188
+ method=method,
189
+ url=path,
190
+ headers=self._get_headers(),
191
+ **kwargs,
192
+ )
193
+ if response.status_code >= 400:
194
+ raise APIError(
195
+ message=response.text or response.reason_phrase,
196
+ status_code=response.status_code,
197
+ )
198
+ return response
199
+ except httpx.TimeoutException as e:
200
+ raise TimeoutError(f"Request to {path} timed out") from e
201
+ except httpx.HTTPError as e:
202
+ raise AssistAgroError(f"HTTP error: {e}") from e
203
+
204
+ async def get(self, path: str, **kwargs: Any) -> httpx.Response:
205
+ """GET request."""
206
+ return await self.request("GET", path, **kwargs)
207
+
208
+ async def post(self, path: str, **kwargs: Any) -> httpx.Response:
209
+ """POST request."""
210
+ return await self.request("POST", path, **kwargs)
211
+
212
+ async def put(self, path: str, **kwargs: Any) -> httpx.Response:
213
+ """PUT request."""
214
+ return await self.request("PUT", path, **kwargs)
215
+
216
+ async def delete(self, path: str, **kwargs: Any) -> httpx.Response:
217
+ """DELETE request."""
218
+ return await self.request("DELETE", path, **kwargs)
@@ -0,0 +1,14 @@
1
+ """Configuration settings."""
2
+
3
+ from pydantic_settings import BaseSettings, SettingsConfigDict
4
+
5
+
6
+ class Settings(BaseSettings):
7
+ model_config = SettingsConfigDict(env_prefix="ASSISTAGRO_")
8
+
9
+ base_url: str = "https://dev-gateway-frontend.agroassist.ru"
10
+ timeout: int = 30
11
+ debug: bool = False
12
+
13
+
14
+ settings = Settings()
@@ -0,0 +1,27 @@
1
+ """Custom exceptions."""
2
+
3
+
4
+ class AssistAgroError(Exception):
5
+ """Base exception for AssistAgro client."""
6
+
7
+ pass
8
+
9
+
10
+ class AuthenticationError(AssistAgroError):
11
+ """Raised when authentication fails."""
12
+
13
+ pass
14
+
15
+
16
+ class APIError(AssistAgroError):
17
+ """Raised when API returns an error."""
18
+
19
+ def __init__(self, message: str, status_code: int):
20
+ self.status_code = status_code
21
+ super().__init__(message)
22
+
23
+
24
+ class TimeoutError(AssistAgroError):
25
+ """Raised when request times out."""
26
+
27
+ pass
@@ -0,0 +1 @@
1
+ """Pydantic models."""
@@ -0,0 +1,79 @@
1
+ Metadata-Version: 2.4
2
+ Name: AssistagroAPI
3
+ Version: 0.1.0
4
+ Summary: Async HTTP client for AssistAgro API
5
+ Author-email: Dmitriy Kazakov <dmitriyfile@yandex.ru>
6
+ License: MIT
7
+ Classifier: License :: OSI Approved :: MIT License
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3.13
10
+ Requires-Python: >=3.13
11
+ Description-Content-Type: text/markdown
12
+ Requires-Dist: httpx>=0.28.1
13
+ Requires-Dist: pydantic>=2.12.5
14
+ Requires-Dist: pydantic-settings>=2.13.1
15
+ Requires-Dist: python-dateutil>=2.9.0.post0
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://dev-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
+ fields = await client.fields.list_()
42
+ print(f"Найдено полей: {len(fields)}")
43
+
44
+ tasks = await client.tasks.list_(limit=10)
45
+ print(f"Найдено задач: {len(tasks)}")
46
+
47
+ if __name__ == "__main__":
48
+ asyncio.run(main())
49
+ ```
50
+
51
+ ## API эндпоинты
52
+
53
+ - **auth** - Аутентификация (sign_in, refresh_tokens, logout)
54
+ - **accounts** - Профили пользователей и аккаунты
55
+ - **companies** - Управление компаниями
56
+ - **fields** - Поля и контуры
57
+ - **tasks** - Управление задачами
58
+ - **techmaps** - Технологические карты
59
+ - **reports** - Отчёты
60
+ - **dictionaries** - Справочники (культуры, пестициды и т.д.)
61
+ - **meteostations** - Метеостанции
62
+ - **structures** - Структуры
63
+
64
+ ## Разработка
65
+
66
+ ```bash
67
+ # Установить зависимости
68
+ uv sync
69
+
70
+ # Запустить тесты
71
+ uv run pytest
72
+
73
+ # Запустить линтер
74
+ uv run ruff check .
75
+ ```
76
+
77
+ ## Лицензия
78
+
79
+ MIT
@@ -0,0 +1,21 @@
1
+ assistagro_client/__init__.py,sha256=JZKliTFyaO-37o2WSnhZJ8hVZnlgYIDYgTzDgnMvbEo,410
2
+ assistagro_client/auth.py,sha256=z1pFZ7B3PWYs2n6XEDzDxPvpi-y8ZtpHIV9QA2ecI0E,3035
3
+ assistagro_client/client.py,sha256=9z76ngXYDtkbr-UPrbClty7zF05nTbgl24rDSfvvFHw,6896
4
+ assistagro_client/config.py,sha256=X4HlaMR-dDhWbneCD66TCQv1B58opprhOUe-frkwVjU,326
5
+ assistagro_client/exceptions.py,sha256=_H5wD4r-bRFtFiyonnUJ_jlUr7QSJtXFhqwfHaS4o_g,518
6
+ assistagro_client/api/__init__.py,sha256=eeTfLUYpfMqfFWpzfPbjzjHIiYLsQsUakOM5ETC_l5A,24
7
+ assistagro_client/api/v1/__init__.py,sha256=eeTfLUYpfMqfFWpzfPbjzjHIiYLsQsUakOM5ETC_l5A,24
8
+ assistagro_client/api/v1/accounts.py,sha256=Z3J5MMyzbJuTn_Sbn3tf5oFEKeRbH3K54QRVW0eWgvw,1430
9
+ assistagro_client/api/v1/companies.py,sha256=5Vk-PyLKorEQd9xRAZfxcK5LIH6E1VQ6IKBK8ydS9EA,821
10
+ assistagro_client/api/v1/dictionaries.py,sha256=dZli1ZLFDZw7ZE7K5FjZ4-aaOcXryDK49fdbyz8kyLw,2478
11
+ assistagro_client/api/v1/fields.py,sha256=tAwCpC6j3sethez4_TdjpixsE2GuGa3QTo1QPIf-L9Q,1625
12
+ assistagro_client/api/v1/meteostations.py,sha256=6Zn6iVgGTN3A7w3c0ntP_YJHGxhNpVGZapxWgv9f050,718
13
+ assistagro_client/api/v1/reports.py,sha256=7Pu3E3OKERvucGZKu22YBTZ_e2FMh367UcuBu_7yNFM,1726
14
+ assistagro_client/api/v1/structures.py,sha256=pGE1CDat_tzHsm808owMUnUJK_Ll7mGYe0wRBYJJJmY,672
15
+ assistagro_client/api/v1/tasks.py,sha256=_6QJSUFcNoFq4V56RJ-Ww3AmVCkIC-heory4vXHlg04,2478
16
+ assistagro_client/api/v1/techmaps.py,sha256=XqfHOZOWx4YQfD9qeSfoKN9-dRei5CKNsH4pwTe6xEM,2105
17
+ assistagro_client/models/__init__.py,sha256=jMtBC6F3WbzMeidPz9xrX0NJRysvfXbnbGP3niuYG3c,23
18
+ assistagroapi-0.1.0.dist-info/METADATA,sha256=UdZRm98g0E9K1h-_tUAqSwEVpVeymT7-dOkJ1g-ET-M,2190
19
+ assistagroapi-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
20
+ assistagroapi-0.1.0.dist-info/top_level.txt,sha256=juHIdi9P2pELResos6x4pj04kflxszIvVi1DDMZMq40,18
21
+ assistagroapi-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ assistagro_client