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.
- assistagro_client/__init__.py +17 -0
- assistagro_client/api/__init__.py +1 -0
- assistagro_client/api/v1/__init__.py +1 -0
- assistagro_client/api/v1/accounts.py +53 -0
- assistagro_client/api/v1/companies.py +39 -0
- assistagro_client/api/v1/dictionaries.py +69 -0
- assistagro_client/api/v1/fields.py +62 -0
- assistagro_client/api/v1/meteostations.py +31 -0
- assistagro_client/api/v1/reports.py +67 -0
- assistagro_client/api/v1/structures.py +30 -0
- assistagro_client/api/v1/tasks.py +93 -0
- assistagro_client/api/v1/techmaps.py +83 -0
- assistagro_client/auth.py +100 -0
- assistagro_client/client.py +218 -0
- assistagro_client/config.py +14 -0
- assistagro_client/exceptions.py +27 -0
- assistagro_client/models/__init__.py +1 -0
- assistagroapi-0.1.0.dist-info/METADATA +79 -0
- assistagroapi-0.1.0.dist-info/RECORD +21 -0
- assistagroapi-0.1.0.dist-info/WHEEL +5 -0
- assistagroapi-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -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 @@
|
|
|
1
|
+
assistagro_client
|