nys-client 0.1.0__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.
@@ -0,0 +1,27 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Noyes
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
23
+
24
+
25
+
26
+
27
+
@@ -0,0 +1,8 @@
1
+ include README.md
2
+ include LICENSE
3
+
4
+
5
+
6
+
7
+
8
+
@@ -0,0 +1,92 @@
1
+ Metadata-Version: 2.4
2
+ Name: nys_client
3
+ Version: 0.1.0
4
+ Summary: Python client library for the Noyes API
5
+ Home-page: https://github.com/noyes/nys_client
6
+ Author: Noyes
7
+ Author-email: dev@noyes.com
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.8
13
+ Classifier: Programming Language :: Python :: 3.9
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Requires-Python: >=3.8
17
+ Description-Content-Type: text/markdown
18
+ License-File: LICENSE
19
+ Requires-Dist: requests>=2.28.1
20
+ Requires-Dist: python-dotenv>=1.0.0
21
+ Requires-Dist: typing-extensions>=4.12.2
22
+ Requires-Dist: pydantic<2.0.0,>=1.10.0
23
+ Requires-Dist: fastapi-pagination>=0.12.0
24
+ Requires-Dist: nys_constants>=0.1.0
25
+ Requires-Dist: nys_schemas>=0.1.0
26
+ Dynamic: author
27
+ Dynamic: author-email
28
+ Dynamic: classifier
29
+ Dynamic: description
30
+ Dynamic: description-content-type
31
+ Dynamic: home-page
32
+ Dynamic: license-file
33
+ Dynamic: requires-dist
34
+ Dynamic: requires-python
35
+ Dynamic: summary
36
+
37
+ # NysClient
38
+
39
+ A Python client library for the Noyes API.
40
+
41
+ ## Installation
42
+
43
+ ```bash
44
+ pip install nys-client
45
+ ```
46
+
47
+ ## Usage
48
+
49
+ ```python
50
+ from nys_client import NysClient, TriggerStatus
51
+
52
+ # Initialize client
53
+ client = NysClient(api_key="your_api_key")
54
+
55
+ # Get requests with filtering
56
+ requests = client.requests.list(
57
+ status="ACCEPTED",
58
+ type="FULFILLMENT",
59
+ sort_by="created_at",
60
+ )
61
+
62
+ # Create a new request
63
+ new_request = client.requests.create(
64
+ type="FULFILLMENT",
65
+ priority=10,
66
+ entities=[{
67
+ "entity_type": "SKU",
68
+ "sku_id": "SKU123",
69
+ "quantity": 1
70
+ }]
71
+ )
72
+
73
+ # Get jobs with search
74
+ jobs = client.jobs.list(
75
+ quick_search="PICKING",
76
+ status="EXECUTING"
77
+ )
78
+
79
+ # Trigger a job
80
+ client.trigger_job(
81
+ job_id="job-uuid",
82
+ trigger_status=TriggerStatus.SUCCEEDED_TRIGGER
83
+ )
84
+ ```
85
+
86
+ ## Development
87
+
88
+ This package is part of the Noyes monorepo. To install in development mode:
89
+
90
+ ```bash
91
+ pip install -e .
92
+ ```
@@ -0,0 +1,56 @@
1
+ # NysClient
2
+
3
+ A Python client library for the Noyes API.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pip install nys-client
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```python
14
+ from nys_client import NysClient, TriggerStatus
15
+
16
+ # Initialize client
17
+ client = NysClient(api_key="your_api_key")
18
+
19
+ # Get requests with filtering
20
+ requests = client.requests.list(
21
+ status="ACCEPTED",
22
+ type="FULFILLMENT",
23
+ sort_by="created_at",
24
+ )
25
+
26
+ # Create a new request
27
+ new_request = client.requests.create(
28
+ type="FULFILLMENT",
29
+ priority=10,
30
+ entities=[{
31
+ "entity_type": "SKU",
32
+ "sku_id": "SKU123",
33
+ "quantity": 1
34
+ }]
35
+ )
36
+
37
+ # Get jobs with search
38
+ jobs = client.jobs.list(
39
+ quick_search="PICKING",
40
+ status="EXECUTING"
41
+ )
42
+
43
+ # Trigger a job
44
+ client.trigger_job(
45
+ job_id="job-uuid",
46
+ trigger_status=TriggerStatus.SUCCEEDED_TRIGGER
47
+ )
48
+ ```
49
+
50
+ ## Development
51
+
52
+ This package is part of the Noyes monorepo. To install in development mode:
53
+
54
+ ```bash
55
+ pip install -e .
56
+ ```
@@ -0,0 +1,116 @@
1
+ """
2
+ A Python client library for the Noyes API.
3
+
4
+ Example:
5
+ >>> from nys_client import NysClient, RequestType, TriggerStatus
6
+ >>>
7
+ >>> # Initialize client
8
+ >>> client = NysClient(api_key="your_api_key")
9
+ >>>
10
+ >>> # Create a request
11
+ >>> new_request = client.requests.create( #TODO
12
+ ... request_type=RequestType.FULFILLMENT,
13
+ ... entities=[("123", 1)]
14
+ ... )
15
+ >>>
16
+ >>> # Search for jobs
17
+ >>> jobs = client.jobs.search(
18
+ ... filters={"status": "EXECUTING", "type": "PICKING"}
19
+ ... )
20
+ >>>
21
+ >>> # Trigger a job
22
+ >>> client.jobs.trigger(
23
+ ... job_id="job-123",
24
+ ... trigger_status=TriggerStatus.SUCCEEDED_TRIGGER
25
+ ... )
26
+ """
27
+
28
+ from .client import NysClient
29
+ from .system import SystemResource
30
+ from .bot import BotResource
31
+
32
+ # Re-export commonly used types from nys_schemas and nys_constants
33
+ from nys_constants.nys_constants import (
34
+ RequestType,
35
+ RequestStatus,
36
+ JobType,
37
+ JobStatus,
38
+ TriggerStatus,
39
+ SortOrder,
40
+ )
41
+
42
+ from nys_schemas.request_schema import (
43
+ RequestResponseComposite,
44
+ RequestFilter,
45
+ FulfillmentRequestCreateInput,
46
+ FetchRequestCreateInput,
47
+ ReplenishmentRequestCreateInput,
48
+ OnboardingRequestCreateInput,
49
+ OffboardingRequestCreateInput,
50
+ RFIDWriteRequestCreateInput,
51
+ PauseRequestCreateInput,
52
+ ResumeRequestCreateInput,
53
+ )
54
+ from nys_schemas.job_schema import (
55
+ JobResponse,
56
+ JobFilter,
57
+ )
58
+ from nys_schemas.bot_schema import (
59
+ Bot,
60
+ BotResponse,
61
+ BotChargingRequest,
62
+ BotChargingResponse,
63
+ BotStopChargingRequest,
64
+ BotStopChargingResponse,
65
+ BotRemoveResponse,
66
+ )
67
+ from nys_schemas.load_schema import (
68
+ LoadResponse,
69
+ LoadCreate,
70
+ LoadPatch,
71
+ )
72
+
73
+ __all__ = [
74
+ # Main client class
75
+ 'NysClient',
76
+
77
+ # Response models - Core entities
78
+ 'RequestResponseComposite',
79
+ 'JobResponse',
80
+ 'BotResponse',
81
+
82
+ # Request/Input models
83
+ 'RequestCreateInputGeneric',
84
+ 'RequestFilter',
85
+ 'JobFilter',
86
+ 'BotChargingRequest',
87
+ 'BotChargingResponse',
88
+ 'BotStopChargingRequest',
89
+ 'BotStopChargingResponse',
90
+ 'BotRemoveResponse',
91
+ 'Bot',
92
+ 'FetchRequestCreateInput',
93
+ 'FulfillmentRequestCreateInput',
94
+ 'ReplenishmentRequestCreateInput',
95
+ 'OnboardingRequestCreateInput',
96
+ 'OffboardingRequestCreateInput',
97
+ 'RFIDWriteRequestCreateInput',
98
+ 'PauseRequestCreateInput',
99
+ 'ResumeRequestCreateInput',
100
+
101
+ # Load models
102
+ 'LoadResponse',
103
+ 'LoadCreate',
104
+ 'LoadPatch',
105
+
106
+
107
+ # Enums and Constants
108
+ 'RequestType',
109
+ 'RequestStatus',
110
+ 'JobType',
111
+ 'JobStatus',
112
+ 'TriggerStatus',
113
+ 'SortOrder',
114
+ ]
115
+
116
+ __version__ = "0.1.0"
@@ -0,0 +1,74 @@
1
+ from __future__ import annotations
2
+ from typing import Any, Dict, Generic, List, Optional, Type, TypeVar, Union, Tuple
3
+ from pydantic import BaseModel
4
+
5
+ from nys_constants.nys_constants import SortOrder
6
+ from .utils import prepare_filter_value
7
+
8
+ T = TypeVar('T', bound=BaseModel)
9
+ FilterType = TypeVar('FilterType', bound=BaseModel)
10
+
11
+ class BaseResource(Generic[T]):
12
+ """Base class for API resources. e.g. Job, Request, Sku, etc"""
13
+
14
+ def __init__(self, client: 'NysClient'):
15
+ self._client = client
16
+
17
+ #TODO consider renaming to _get
18
+ def _search(
19
+ self,
20
+ *,
21
+ endpoint: str,
22
+ response_model: Type[T],
23
+ filters: Optional[FilterType] = None,
24
+ search: Optional[str] = None,
25
+ sort_by: Optional[Union[str, List[Tuple[str, SortOrder]]]] = None,
26
+ page: int = 1,
27
+ size: int = 50
28
+ ) -> List[T]:
29
+ """Generic search method used by all resource-specific search methods."""
30
+ params: Dict[str, Any] = {
31
+ "page": page,
32
+ "size": size
33
+ }
34
+
35
+ # Add sorting parameters if provided
36
+ if sort_by:
37
+ if isinstance(sort_by, str):
38
+ params["sort_by"] = sort_by
39
+ else:
40
+ # Convert list of tuples to comma-separated string with prefixes
41
+ sort_str = ','.join(
42
+ f"{'-' if order == SortOrder.DESC else ''}{field}"
43
+ for field, order in sort_by
44
+ )
45
+ params["sort_by"] = sort_str
46
+
47
+ # Add search fields if provided
48
+ if search:
49
+ if isinstance(search, str):
50
+ # leave the validation to the API
51
+ params['search'] = search
52
+ else:
53
+ raise ValueError("search must be a string")
54
+
55
+ # Add filters if provided
56
+ if filters:
57
+ # Convert filter model to dict and remove None values
58
+ filter_dict = filters.dict(exclude_unset=True, exclude_defaults=True)
59
+
60
+ # Add each non-None filter to params, converting values as needed
61
+ for field_name, value in filter_dict.items():
62
+ if value is not None:
63
+ params[field_name] = prepare_filter_value(value)
64
+
65
+ # Make API request
66
+ response = self._client._request(
67
+ method="GET",
68
+ endpoint=endpoint,
69
+ params=params
70
+ )
71
+
72
+ # Parse response into list of model instances
73
+ data = response.json()
74
+ return [response_model(**item) for item in data["items"]]
@@ -0,0 +1,132 @@
1
+ from __future__ import annotations
2
+ from typing import Any, Dict, List, Optional
3
+ from nys_schemas.bot_schema import (
4
+ Bot,
5
+ BotResponse,
6
+ BotFilter,
7
+ BotSort,
8
+ BotChargingRequest,
9
+ BotChargingResponse,
10
+ BotStopChargingRequest,
11
+ BotStopChargingResponse,
12
+ BotRemoveResponse
13
+ )
14
+ from .base import BaseResource
15
+
16
+ __all__ = [
17
+ 'BotResource', # The main resource class
18
+ 'Bot',
19
+ 'BotResponse',
20
+ 'BotFilter',
21
+ 'BotSort',
22
+ 'BotChargingRequest',
23
+ 'BotChargingResponse',
24
+ 'BotStopChargingRequest',
25
+ 'BotStopChargingResponse',
26
+ 'BotRemoveResponse'
27
+ ]
28
+
29
+ class BotResource(BaseResource[BotResponse]):
30
+ """Resource for interacting with Bots."""
31
+
32
+ def get_bots(self) -> List[BotResponse]:
33
+ """
34
+ Get all bots.
35
+
36
+ Returns:
37
+ List[BotResponse]: List of all bots in the system
38
+
39
+ Examples:
40
+ >>> bots = client.bots.get_bots()
41
+ >>> for bot in bots:
42
+ ... print(f"Bot ID: {bot.idx}, Level: {bot.level_id}, Battery: {bot.battery_level}%")
43
+ """
44
+ response = self._client._request(
45
+ method="GET",
46
+ endpoint="api/v2/bots"
47
+ )
48
+ return [BotResponse(**item) for item in response.json()]
49
+
50
+ def go_charging(self, charging_request: BotChargingRequest) -> BotChargingResponse:
51
+ """
52
+ Send specified bots to charging stations.
53
+
54
+ This endpoint proxies the request to the brain API which handles the ROS communication.
55
+
56
+ Args:
57
+ charging_request: The request parameters including bot_idxs, force flag,
58
+ block_until_done flag, and timeout
59
+
60
+ Returns:
61
+ BotChargingResponse: Response with success/failure information
62
+
63
+ Examples:
64
+ >>> from nys_client.bot import BotChargingRequest
65
+ >>> response = client.bots.go_charging(
66
+ ... BotChargingRequest(
67
+ ... bot_ids=["1", "2"],
68
+ ... force=True,
69
+ ... block_until_done=False,
70
+ ... timeout=180
71
+ ... )
72
+ ... )
73
+ >>> print(f"Success: {response.success}, Message: {response.message}")
74
+ """
75
+ response = self._client._request(
76
+ method="POST",
77
+ endpoint="api/v2/bot/go_charging",
78
+ json=charging_request.dict()
79
+ )
80
+ return BotChargingResponse(**response.json())
81
+
82
+ def stop_charging(self, stop_charging_request: BotStopChargingRequest) -> BotStopChargingResponse:
83
+ """
84
+ Stop charging for specified bots.
85
+
86
+ This endpoint proxies the request to the brain API which handles the ROS communication.
87
+
88
+ Args:
89
+ stop_charging_request: The request parameters including bot_idxs and force flag
90
+
91
+ Returns:
92
+ BotStopChargingResponse: Response with success/failure information
93
+
94
+ Examples:
95
+ >>> from nys_client.bot import BotStopChargingRequest
96
+ >>> response = client.bots.stop_charging(
97
+ ... BotStopChargingRequest(
98
+ ... bot_ids=["1", "2"],
99
+ ... force=True
100
+ ... )
101
+ ... )
102
+ >>> print(f"Success: {response.success}, Message: {response.message}")
103
+ """
104
+ response = self._client._request(
105
+ method="POST",
106
+ endpoint="api/v2/bot/stop_charging",
107
+ json=stop_charging_request.dict()
108
+ )
109
+ return BotStopChargingResponse(**response.json())
110
+
111
+ def remove_bots(self, bot_ids: List[str]) -> BotRemoveResponse:
112
+ """
113
+ Remove specified bots from the database.
114
+
115
+ Warning: This is not the usual offboarding process. Use at your own risk!
116
+
117
+ Args:
118
+ bot_ids: List of bot IDs to remove from database; use ["*"] for all bots
119
+
120
+ Returns:
121
+ BotRemoveResponse: Response with success/failure information
122
+
123
+ Examples:
124
+ >>> response = client.bots.remove_bots(["1", "2"])
125
+ >>> print(f"Success: {response.success}, Message: {response.message}")
126
+ """
127
+ response = self._client._request(
128
+ method="DELETE",
129
+ endpoint="api/v2/bot",
130
+ params={"bot_ids": bot_ids}
131
+ )
132
+ return BotRemoveResponse(**response.json())
@@ -0,0 +1,139 @@
1
+ from __future__ import annotations
2
+ import logging
3
+ import requests
4
+ from typing import Optional
5
+
6
+ from .request import RequestResource
7
+ from .job import JobResource
8
+ from .skus import SkuResource
9
+ from .loads import LoadResource
10
+ from .requests_and_jobs_view import RequestsAndJobsViewResource
11
+ from .inventory_view import InventoryViewResource
12
+ from .system import SystemResource
13
+ from .bot import BotResource
14
+ from .events import EventResource
15
+ logger = logging.getLogger(__name__)
16
+ from requests.adapters import HTTPAdapter
17
+ from urllib3.util.retry import Retry
18
+
19
+ class NysClient:
20
+ """Client for interacting with the Noyes API."""
21
+
22
+ def __init__(
23
+ self,
24
+ api_key: str,
25
+ base_url: str = "http://noyes-api:8003",
26
+ # TODO: Increased timeout to 60 seconds for long-running requests. Eg. stop_brain endpoint
27
+ timeout: int = 60 # seconds
28
+ ):
29
+ """
30
+ Initialize the NysClient.
31
+
32
+ Args:
33
+ api_key: Your API key
34
+ base_url: Base URL of the API (default: http://noyes-api:8003)
35
+ timeout: Request timeout in seconds (default: 60)
36
+ """
37
+ self.api_key = api_key
38
+ self.base_url = base_url.rstrip("/")
39
+ self.timeout = timeout
40
+
41
+ self._headers = {
42
+ "Authorization": f"Bearer {api_key}",
43
+ "Content-Type": "application/json",
44
+ }
45
+
46
+ # Initialize resources
47
+ self._requests = RequestResource(self)
48
+ self._jobs = JobResource(self)
49
+ self._skus = SkuResource(self)
50
+ self._loads = LoadResource(self)
51
+ self._requests_and_jobs_view = RequestsAndJobsViewResource(self)
52
+ self._inventory_view = InventoryViewResource(self)
53
+ self._system = SystemResource(self)
54
+ self._bots = BotResource(self)
55
+ self._events = EventResource(self)
56
+
57
+ @property
58
+ def requests(self) -> RequestResource:
59
+ """Access request-related operations."""
60
+ return self._requests
61
+
62
+ @property
63
+ def jobs(self) -> JobResource:
64
+ """Access job-related operations."""
65
+ return self._jobs
66
+
67
+ @property
68
+ def skus(self) -> SkuResource:
69
+ """Access SKU-related operations."""
70
+ return self._skus
71
+
72
+ @property
73
+ def loads(self) -> LoadResource:
74
+ """Access load-related operations."""
75
+ return self._loads
76
+
77
+ @property
78
+ def requests_and_jobs_view(self) -> RequestsAndJobsViewResource:
79
+ """Access requests and jobs view operations."""
80
+ return self._requests_and_jobs_view
81
+
82
+ @property
83
+ def inventory_view(self) -> InventoryViewResource:
84
+ """Access inventory view operations."""
85
+ return self._inventory_view
86
+
87
+ @property
88
+ def system(self) -> SystemResource:
89
+ """Access system-related operations."""
90
+ return self._system
91
+
92
+ @property
93
+ def bots(self) -> BotResource:
94
+ """Access bot-related operations."""
95
+ return self._bots
96
+
97
+ @property
98
+ def events(self) -> EventResource:
99
+ """Access event-related operations."""
100
+ return self._events
101
+
102
+ def _request(
103
+ self,
104
+ method: str,
105
+ endpoint: str,
106
+ params: Optional[dict] = None,
107
+ json: Optional[dict] = None,
108
+ **kwargs
109
+ ) -> requests.Response:
110
+ """
111
+ Make a request to the API.
112
+
113
+ Args:
114
+ method: HTTP method (GET, POST, etc.)
115
+ endpoint: API endpoint
116
+ params: Query parameters
117
+ json: JSON body
118
+ **kwargs: Additional arguments to pass to requests
119
+
120
+ Returns:
121
+ requests.Response: Response from the API
122
+ """
123
+ url = f"{self.base_url}/{endpoint.lstrip('/')}"
124
+ response = requests.request(
125
+ method=method,
126
+ url=url,
127
+ params=params,
128
+ json=json,
129
+ timeout=self.timeout,
130
+ headers=self._headers,
131
+ **kwargs
132
+ )
133
+ try:
134
+ response.raise_for_status()
135
+ except requests.HTTPError as e:
136
+ logger.error(f"HTTP error occurred: {e}")
137
+ logger.error(f"Response content: {response.text}")
138
+ raise e
139
+ return response