Skip to content

Commit

Permalink
Merge pull request #10 from nextmv-io/feature/eng-4408-read-the-api-k…
Browse files Browse the repository at this point in the history
…ey-from-a-hierarchy

ENG-4408 Look for the API Key according to a hierarchy
  • Loading branch information
sebastian-quintero authored Jan 17, 2024
2 parents 3937da5 + 69c1563 commit b5181a7
Show file tree
Hide file tree
Showing 8 changed files with 87 additions and 22 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -158,3 +158,6 @@ cython_debug/
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/

# VSCode
.vscode/
11 changes: 6 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ Visit the [docs][docs] for more information.

## Installation

Install using `pip`:
Requires Python `>=3.10`. Install using `pip`:

```bash
pip install nextmv
Expand All @@ -20,17 +20,17 @@ pip install nextmv
Make sure that you have your API key set as an environment variable:

```bash
export NEXTMV_API_KEY=<your-API-key>
export NEXTMV_API_KEY="<YOUR-API-KEY>"
```

Additionally, you must have a valid app in the Nextmv Cloud.
Additionally, you must have a valid app in Nextmv Cloud.

- Make a run and get the results.

```python
import os

from nextmv.cloud import Application, Client
from nextmv.cloud import Application, Client, PollingOptions

input = {
"defaults": {"vehicles": {"speed": 20}},
Expand All @@ -56,11 +56,12 @@ input = {
}

client = Client(api_key=os.getenv("NEXTMV_API_KEY"))
app = Application(client=client, id="your-app-id")
app = Application(client=client, id="<YOUR-APP-ID>")
result = app.new_run_with_result(
input=input,
instance_id="latest",
run_options={"solve.duration": "1s"},
polling_options=PollingOptions(), # Customize the polling options.
)
print(result.to_dict())

Expand Down
7 changes: 5 additions & 2 deletions nextmv/cloud/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -443,7 +443,7 @@ def new_input_set(

def new_run(
self,
input: dict[str, Any] = None,
input: dict[str, Any] | BaseModel = None,
instance_id: str | None = None,
name: str | None = None,
description: str | None = None,
Expand All @@ -470,6 +470,9 @@ def new_run(
requests.HTTPError: If the response status code is not 2xx.
"""

if isinstance(input, BaseModel):
input = input.to_dict()

input_size = 0
if input is not None:
input_size = get_size(input)
Expand Down Expand Up @@ -508,7 +511,7 @@ def new_run(

def new_run_with_result(
self,
input: dict[str, Any] = None,
input: dict[str, Any] | BaseModel = None,
instance_id: str | None = None,
name: str | None = None,
description: str | None = None,
Expand Down
61 changes: 46 additions & 15 deletions nextmv/cloud/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from urllib.parse import urljoin

import requests
import yaml
from requests.adapters import HTTPAdapter, Retry

_MAX_LAMBDA_PAYLOAD_SIZE: int = 500 * 1024 * 1024
Expand All @@ -16,20 +17,21 @@
@dataclass
class Client:
"""
Client that interacts directly with the Nextmv Cloud API. The API key
must be provided either in the constructor or via the NEXTMV_API_KEY
environment variable.
Client that interacts directly with the Nextmv Cloud API. The API key will
be searched, in order of precedence, in: the api_key arg in the
constructor, the NEXTMV_API_KEY environment variable, the
~/.nextmv/config.yaml file used by the Nextmv CLI.
"""

api_key: str | None = None
"""API key to use for authenticating with the Nextmv Cloud API. If not
provided, the client will look for the NEXTMV_API_KEY environment
variable."""
allowed_methods: list[str] = field(
default_factory=lambda: ["GET", "POST", "PUT", "DELETE"],
)
"""Allowed HTTP methods to use for retries in requests to the Nextmv Cloud
API."""
api_key: str | None = None
"""API key to use for authenticating with the Nextmv Cloud API. If not
provided, the client will look for the NEXTMV_API_KEY environment
variable."""
backoff_factor: float = 1
"""Exponential backoff factor to use for requests to the Nextmv Cloud
API."""
Expand All @@ -38,6 +40,8 @@ class Client:
backoff_max: float = 60
"""Maximum backoff time to use for requests to the Nextmv Cloud API, in
seconds."""
configuration_file: str = "~/.nextmv/config.yaml"
"""Path to the configuration file used by the Nextmv CLI."""
headers: dict[str, str] | None = None
"""Headers to use for requests to the Nextmv Cloud API."""
max_retries: int = 10
Expand All @@ -55,14 +59,41 @@ class Client:
def __post_init__(self):
"""Logic to run after the class is initialized."""

if self.api_key is None:
api_key = os.getenv("NEXTMV_API_KEY")
if api_key is None:
raise ValueError(
"no API key provided. Either set it in the constructor or "
"set the NEXTMV_API_KEY environment variable."
)
self.api_key = api_key
if self.api_key is not None and self.api_key != "":
return

if self.api_key == "":
raise ValueError("api_key cannot be empty")

api_key_env = os.getenv("NEXTMV_API_KEY")
if api_key_env is not None:
self.api_key = api_key_env
return

config_path = os.path.expanduser(self.configuration_file)
if not os.path.exists(config_path):
raise ValueError(
"no API key set in constructor or NEXTMV_API_KEY env var, and ~/.nextmv/config.yaml does not exist"
)

with open(config_path) as f:
config = yaml.safe_load(f)

profile = os.getenv("NEXTMV_PROFILE")
parent = config
if profile is not None:
parent = config.get(profile)
if parent is None:
raise ValueError(f"profile {profile} set via NEXTMV_PROFILE but not found in ~/.nextmv/config.yaml")

api_key = parent.get("apikey")
if api_key is None:
raise ValueError("no apiKey found in ~/.nextmv/config.yaml")
self.api_key = api_key

endpoint = parent.get("endpoint")
if endpoint is not None:
self.url = f"https://{endpoint}"

self.headers = {
"Authorization": f"Bearer {self.api_key}",
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ classifiers = [
dependencies = [
"pydantic>=2.5.2",
"requests>=2.31.0",
"pyyaml>=6.0.1",
]
description = "The Python SDK for Nextmv"
dynamic = [
Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
build>=1.0.3
pydantic>=2.5.2
pyyaml>=6.0.1
requests>=2.31.0
ruff>=0.1.7
twine>=4.0.2
Empty file added tests/cloud/__init__.py
Empty file.
25 changes: 25 additions & 0 deletions tests/cloud/test_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import os
import unittest

from nextmv.cloud import Client


class TestClient(unittest.TestCase):
def test_api_key(self):
client1 = Client(api_key="foo")
self.assertEqual(client1.api_key, "foo")

os.environ["NEXTMV_API_KEY"] = "bar"
client2 = Client()
self.assertEqual(client2.api_key, "bar")
os.environ.pop("NEXTMV_API_KEY")

with self.assertRaises(ValueError):
Client(api_key="")

with self.assertRaises(ValueError):
Client(configuration_file="")

os.environ["NEXTMV_PROFILE"] = "i-like-turtles"
with self.assertRaises(ValueError):
Client()

0 comments on commit b5181a7

Please sign in to comment.