Skip to content

Commit 14e361c

Browse files
Merge pull request #2 from nextmv-io/feature/eng-4278-create-the-functionality-for-solving-remotely
2 parents ca6ac17 + 10a9ce6 commit 14e361c

File tree

13 files changed

+380
-74
lines changed

13 files changed

+380
-74
lines changed

nextmv-py.code-workspace

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
}
66
],
77
"settings": {
8-
"python.testing.unittestArgs": ["-v", "-s", "./tests", "-p", "test*.py"],
8+
"python.testing.unittestArgs": ["-v", "-s", ".", "-p", "test*.py"],
99
"python.testing.pytestEnabled": false,
1010
"python.testing.unittestEnabled": true
1111
}

nextmv/base_model.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
"""JSON class for data wrangling JSON objects."""
2+
3+
from typing import Any
4+
5+
from pydantic import BaseModel
6+
7+
8+
class BaseModel(BaseModel):
9+
"""Base class for data wrangling tasks with JSON."""
10+
11+
@classmethod
12+
def from_dict(cls, data: dict[str, Any]):
13+
"""Instantiates the class from a dict."""
14+
15+
return cls(**data)
16+
17+
def to_dict(self) -> dict[str, Any]:
18+
"""Converts the class to a dict."""
19+
20+
return self.model_dump(mode="json", exclude_none=True)

nextmv/cloud/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
"""Functionality for interacting with the Nextmv Cloud."""
2+
3+
from .application import Application as Application
4+
from .client import Client as Client

nextmv/cloud/application.py

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
"""This module contains the application class."""
2+
3+
from dataclasses import dataclass
4+
from datetime import datetime
5+
from typing import Any
6+
7+
from nextmv.base_model import BaseModel
8+
from nextmv.cloud.client import Client
9+
10+
11+
class Metadata(BaseModel):
12+
"""Metadata of a run, whether it was successful or not."""
13+
14+
status: str
15+
"""Status of the run."""
16+
created_at: datetime
17+
"""Date and time when the run was created."""
18+
duration: float
19+
"""Duration of the run in milliseconds."""
20+
input_size: float
21+
"""Size of the input in bytes."""
22+
output_size: float
23+
"""Size of the output in bytes."""
24+
error: str
25+
"""Error message if the run failed."""
26+
application_id: str
27+
"""ID of the application where the run was submitted to."""
28+
application_instance_id: str
29+
"""ID of the instance where the run was submitted to."""
30+
application_version_id: str
31+
"""ID of the version of the application where the run was submitted to."""
32+
33+
34+
class RunResult(BaseModel):
35+
"""Result of a run, wheter it was successful or not."""
36+
37+
id: str
38+
"""ID of the run."""
39+
user_email: str
40+
"""Email of the user who submitted the run."""
41+
name: str
42+
"""Name of the run."""
43+
description: str
44+
"""Description of the run."""
45+
metadata: Metadata
46+
"""Metadata of the run."""
47+
output: dict[str, Any]
48+
"""Output of the run."""
49+
50+
51+
@dataclass
52+
class Application:
53+
"""An application is a published decision model that can be executed."""
54+
55+
client: Client
56+
"""Client to use for interacting with the Nextmv Cloud API."""
57+
id: str
58+
"""ID of the application."""
59+
endpoint: str = "v1/applications/{id}"
60+
"""Base endpoint for the application."""
61+
default_instance_id: str = "devint"
62+
"""Default instance ID to use for submitting runs."""
63+
64+
def __post_init__(self):
65+
"""Logic to run after the class is initialized."""
66+
67+
self.endpoint = self.endpoint.format(id=self.id)
68+
69+
def new_run(
70+
self,
71+
input: dict[str, Any] = None,
72+
instance_id: str | None = None,
73+
name: str | None = None,
74+
description: str | None = None,
75+
upload_id: str | None = None,
76+
options: dict[str, Any] | None = None,
77+
) -> str:
78+
"""
79+
Submit an input to start a new run of the application. Returns the
80+
run_id of the submitted run.
81+
82+
Args:
83+
input: Input to use for the run.
84+
instance_id: ID of the instance to use for the run. If not
85+
provided, the default_instance_id will be used.
86+
name: Name of the run.
87+
description: Description of the run.
88+
upload_id: ID to use when running a large input.
89+
options: Options to use for the run.
90+
91+
Returns:
92+
ID of the submitted run.
93+
"""
94+
95+
payload = {}
96+
if input is not None:
97+
payload["input"] = input
98+
if name is not None:
99+
payload["name"] = name
100+
if description is not None:
101+
payload["description"] = description
102+
if upload_id is not None:
103+
payload["upload_id"] = upload_id
104+
if options is not None:
105+
payload["options"] = options
106+
107+
query_params = {
108+
"instance_id": instance_id if instance_id is not None else self.default_instance_id,
109+
}
110+
response = self.client.post(
111+
endpoint=f"{self.endpoint}/runs",
112+
payload=payload,
113+
query_params=query_params,
114+
)
115+
116+
return response.json()["run_id"]
117+
118+
def run_result(
119+
self,
120+
run_id: str,
121+
) -> RunResult:
122+
"""
123+
Get the result of a run.
124+
125+
Args:
126+
run_id: ID of the run.
127+
128+
Returns:
129+
Result of the run.
130+
"""
131+
132+
response = self.client.get(
133+
endpoint=f"{self.endpoint}/runs/{run_id}",
134+
)
135+
136+
return RunResult.from_dict(response.json())

nextmv/cloud/client.py

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
"""Module with the client class."""
2+
3+
import os
4+
from dataclasses import dataclass
5+
from typing import Any
6+
7+
import requests
8+
9+
10+
@dataclass
11+
class Client:
12+
"""
13+
Client that interacts directly with the Nextmv Cloud API. The API key
14+
must be provided either in the constructor or via the NEXTMV_API_KEY
15+
environment variable.
16+
"""
17+
18+
api_key: str | None = None
19+
"""API key to use for authenticating with the Nextmv Cloud API. If not
20+
provided, the client will look for the NEXTMV_API_KEY environment
21+
variable."""
22+
url: str = "https://api.cloud.nextmv.io"
23+
"""URL of the Nextmv Cloud API."""
24+
headers: dict[str, str] | None = None
25+
"""Headers to use for requests to the Nextmv Cloud API."""
26+
27+
def __post_init__(self):
28+
"""Logic to run after the class is initialized."""
29+
30+
if self.api_key is None:
31+
api_key = os.getenv("NEXTMV_API_KEY")
32+
if api_key is None:
33+
raise ValueError(
34+
"no API key provided. Either set it in the constructor or "
35+
"set the NEXTMV_API_KEY environment variable."
36+
)
37+
self.api_key = api_key
38+
39+
self.headers = {
40+
"Authorization": f"Bearer {self.api_key}",
41+
"Content-Type": "application/json",
42+
}
43+
44+
def post(
45+
self,
46+
endpoint: str,
47+
payload: dict[str, Any],
48+
query_params: dict[str, Any] | None = None,
49+
) -> requests.Response:
50+
"""
51+
Send a POST request to the Nextmv Cloud API.
52+
53+
Args:
54+
endpoint: Endpoint to send the request to.
55+
payload: Payload to send with the request.
56+
query_params: Query parameters to send with the request.
57+
58+
Returns:
59+
Response from the Nextmv Cloud API.
60+
"""
61+
62+
response = requests.post(
63+
url=f"{self.url}/{endpoint}",
64+
json=payload,
65+
headers=self.headers,
66+
params=query_params,
67+
)
68+
response.raise_for_status()
69+
70+
return response
71+
72+
def get(
73+
self,
74+
endpoint: str,
75+
query_params: dict[str, Any] | None = None,
76+
) -> requests.Response:
77+
"""
78+
Send a GET request to the Nextmv Cloud API.
79+
80+
Args:
81+
endpoint: Endpoint to send the request to.
82+
query_params: Query parameters to send with the request.
83+
84+
Returns:
85+
Response from the Nextmv Cloud API.
86+
"""
87+
88+
response = requests.get(
89+
url=f"{self.url}/{endpoint}",
90+
headers=self.headers,
91+
params=query_params,
92+
)
93+
response.raise_for_status()
94+
95+
return response

nextmv/nextroute/schema/base.py

Lines changed: 0 additions & 33 deletions
This file was deleted.

nextmv/nextroute/schema/input.py

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,13 @@
11
"""Defines the input class"""
22

3-
from dataclasses import dataclass
43
from typing import Any
54

6-
from .base import _Base
7-
from .stop import AlternateStop, Stop, StopDefaults
8-
from .vehicle import Vehicle, VehicleDefaults
5+
from nextmv.base_model import BaseModel
6+
from nextmv.nextroute.schema.stop import AlternateStop, Stop, StopDefaults
7+
from nextmv.nextroute.schema.vehicle import Vehicle, VehicleDefaults
98

109

11-
@dataclass
12-
class Defaults(_Base):
10+
class Defaults(BaseModel):
1311
"""Default values for vehicles and stops."""
1412

1513
stops: StopDefaults | None = None
@@ -18,8 +16,7 @@ class Defaults(_Base):
1816
"""Default values for vehicles."""
1917

2018

21-
@dataclass
22-
class DurationGroup(_Base):
19+
class DurationGroup(BaseModel):
2320
"""Represents a group of stops that get additional duration whenever a stop
2421
of the group is approached for the first time."""
2522

@@ -29,8 +26,7 @@ class DurationGroup(_Base):
2926
"""Stop IDs contained in the group."""
3027

3128

32-
@dataclass
33-
class Input(_Base):
29+
class Input(BaseModel):
3430
"""Input schema for Nextroute."""
3531

3632
stops: list[Stop]

nextmv/nextroute/schema/location.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,10 @@
11
"""Defines the location class."""
22

33

4-
from dataclasses import dataclass
4+
from nextmv.base_model import BaseModel
55

6-
from .base import _Base
76

8-
9-
@dataclass
10-
class Location(_Base):
7+
class Location(BaseModel):
118
"""Location represents a geographical location."""
129

1310
lon: float

nextmv/nextroute/schema/stop.py

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,14 @@
11
"""Defines the stop class."""
22

33

4-
from dataclasses import dataclass
54
from datetime import datetime
65
from typing import Any
76

8-
from .base import _Base
9-
from .location import Location
7+
from nextmv.base_model import BaseModel
8+
from nextmv.nextroute.schema.location import Location
109

1110

12-
@dataclass
13-
class StopDefaults(_Base):
11+
class StopDefaults(BaseModel):
1412
"""Default values for a stop."""
1513

1614
compatibility_attributes: list[str] | None = None
@@ -33,7 +31,6 @@ class StopDefaults(_Base):
3331
"""Penalty for not planning a stop."""
3432

3533

36-
@dataclass(kw_only=True)
3734
class Stop(StopDefaults):
3835
"""Stop is a location that must be visited by a vehicle in a Vehicle
3936
Routing Problem (VRP.)"""
@@ -53,7 +50,6 @@ class Stop(StopDefaults):
5350
"""Stops that must be visited before this one on the same route."""
5451

5552

56-
@dataclass(kw_only=True)
5753
class AlternateStop(StopDefaults):
5854
"""An alternate stop can be serviced instead of another stop."""
5955

0 commit comments

Comments
 (0)