Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 26 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,15 @@ For full documentation, please visit our [dedicated](https://armis-python-sdk.re

## Usage

All interaction with the SDK happens through the `ArmisSdk` class. You'll need three things:
All interaction with the SDK happens through the `ArmisSdk` class. You'll need five things:

1. **Tenant name**: The name of the tenant you want to interact with.
2. **Secret key**: The secret key associated with the tenant, obtained from the tenant itself.
3. **Client id**: A unique identifier for your application. Currently, this can be any string.
1. **Audience**: The url of the tenant you want to interact with, including trailing slash (e.g. `https://acme.armis.com/`).
2. **Client ID**: The email address of the user account within the tenant that was used to generate the Client Secret.
3. **Client Secret**: The confidential credential generated by your customer within Armis, paired with the Client ID.
4. **Vendor ID**: An identifier unique to your developer account or integration, obtained when you register on our developer portal.
5. **Scopes**: The specific permissions required by your access token to interact with the desired API endpoints.

You can either provide these values using the environment variables `ARMIS_TENANT`, `ARMIS_SECRET_KEY`, and `ARMIS_CLIENT_ID`:
You can either provide these values using the environment variables `ARMIS_AUDIENCE`, `ARMIS_CLIENT_ID`, `ARMIS_CLIENT_SECRET`, `ARMIS_VENDOR_ID`, and `ARMIS_CLIENT_ID`:
```python
from armis_sdk import ArmisSdk

Expand All @@ -32,16 +34,30 @@ armis_sdk = ArmisSdk()
or by passing them explicitly:
```python
from armis_sdk import ArmisSdk

armis_sdk = ArmisSdk(tenant="<tenant>", secret_key="<secret_key>", client_id="<client_id>")
from armis_sdk import ClientCredentials

credentials = ClientCredentials(
audience="<audience>",
client_id="<client_id>",
client_secret="<client_secret>",
vendor_id="<vendor_id>",
scopes=["scope1", "scope2"],
)
armis_sdk = ArmisSdk(credentials=credentials)
```

> [!TIP]
> If you're building an application that interacts with multiple tenants, you can populate only the `ARMIS_CLIENT_ID` environment variable and pass the `tenant` and `secret_key` explicitly:
> If you're building an application that interacts with multiple tenants, you can populate only the `ARMIS_VENDOR_ID` and `ARMIS_SCOPES` environment variable and pass the `audience`, `client_id` and `client_secret` explicitly:
> ```python
> from armis_sdk import ArmisSdk
> from armis_sdk import ClientCredentials
>
> armis_sdk = ArmisSdk(tenant="<tenant>", secret_key="<secret_key>")
> credentials = ClientCredentials(
> audience="<audience>",
> client_id="<client_id>",
> client_secret="<client_secret>",
> )
> armis_sdk = ArmisSdk(credentials=credentials)
> ```

## Entity clients
Expand All @@ -63,7 +79,7 @@ from armis_sdk.entities.site import Site
armis_sdk = ArmisSdk()

async def main():
site = Site(id="1", location="new location")
site = Site(id=1, location="new location")
await armis_sdk.sites.update(site)

asyncio.run(main())
Expand Down
1 change: 1 addition & 0 deletions armis_sdk/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
from armis_sdk.core.armis_sdk import ArmisSdk
from armis_sdk.core.client_credentials import ClientCredentials
58 changes: 15 additions & 43 deletions armis_sdk/clients/sites_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@

import universalasync

from armis_sdk.clients.network_equipment_client import NetworkEquipmentClient
from armis_sdk.clients.site_integrations_client import SiteIntegrationsClient
from armis_sdk.core import response_utils
from armis_sdk.core.armis_error import ArmisError
from armis_sdk.core.base_entity_client import BaseEntityClient
Expand All @@ -18,17 +16,8 @@ class SitesClient(BaseEntityClient):
A client for interacting with sites.

The primary entity for this client is [Site][armis_sdk.entities.site.Site].

Attributes:
network_equipment_client (NetworkEquipmentClient): An instance of [NetworkEquipmentClient][armis_sdk.clients.network_equipment_client.NetworkEquipmentClient]
site_integrations_client (SiteIntegrationsClient): An instance of [SiteIntegrationsClient][armis_sdk.clients.site_integrations_client.SiteIntegrationsClient]
"""

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.network_equipment_client = NetworkEquipmentClient(self._armis_client)
self.site_integrations_client = SiteIntegrationsClient(self._armis_client)

async def create(self, site: Site) -> Site:
"""Create a `Site`.

Expand Down Expand Up @@ -69,21 +58,13 @@ async def main():
raise ArmisError("Can't create a site without a name.")

payload = site.model_dump(
by_alias=True,
exclude={"children", "network_equipment_device_ids"},
exclude={"children"},
exclude_none=True,
)
async with self._armis_client.client() as client:
response = await client.post("/api/v1/sites/", json=payload)
response = await client.post("/v3/settings/sites", json=payload)
data = response_utils.get_data_dict(response)
created_site = site.model_copy(update={"id": int(data["id"])}, deep=True)

if site.network_equipment_device_ids:
await self.network_equipment_client.add(
created_site, site.network_equipment_device_ids
)

return created_site
return Site.model_validate(data)

async def delete(self, site: Site):
"""Delete a `Site`.
Expand Down Expand Up @@ -112,10 +93,10 @@ async def main():
raise ArmisError("Can't delete a site without an id.")

async with self._armis_client.client() as client:
response = await client.delete(f"/api/v1/sites/{site.id}/")
response = await client.delete(f"/v3/settings/sites/{site.id}")
response_utils.raise_for_status(response)

async def get(self, site_id: str) -> Site:
async def get(self, site_id: int) -> Site:
"""Get a `Site` by its ID.

Args:
Expand Down Expand Up @@ -144,7 +125,7 @@ async def main():
```
"""
async with self._armis_client.client() as client:
response = await client.get(f"/api/v1/sites/{site_id}/")
response = await client.get(f"/v3/settings/sites/{site_id}")
data = response_utils.get_data_dict(response)
return Site.model_validate(data)

Expand Down Expand Up @@ -218,10 +199,10 @@ async def main():
Site(id=2)
```
"""
async for item in self._list("/api/v1/sites/", "sites", Site):
async for item in self._list("/v3/settings/sites", Site):
yield item

async def update(self, site: Site):
async def update(self, site: Site) -> Site:
"""Update a site's properties.

Args:
Expand Down Expand Up @@ -253,23 +234,14 @@ async def main():
)

data = site.model_dump(
by_alias=True,
exclude={
"children",
"id",
"integration_ids",
"network_equipment_device_ids",
},
exclude={"children", "id"},
exclude_none=True,
)

if data:
async with self._armis_client.client() as client:
response = await client.patch(f"/api/v1/sites/{site.id}/", json=data)
response_utils.raise_for_status(response)

if site.network_equipment_device_ids is not None:
await self.network_equipment_client.update(site)
if not data:
return site

if site.integration_ids is not None:
await self.site_integrations_client.update(site)
async with self._armis_client.client() as client:
response = await client.patch(f"/v3/settings/sites/{site.id}", json=data)
data = response_utils.get_data_dict(response)
return Site.model_validate(data)
28 changes: 19 additions & 9 deletions armis_sdk/core/armis_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

from armis_sdk.core import response_utils
from armis_sdk.core.armis_error import ArmisError
from armis_sdk.core.client_credentials import ClientCredentials

AUTHORIZATION = "Authorization"

Expand All @@ -17,16 +18,16 @@ class ArmisAuth(httpx.Auth):

1. Before performing any request check if there's a valid access token.
2. If there is, use it with the `Authorization` header.
3. If there isn't, make a POST request to `/api/v1/access_token/`
3. If there isn't, make a POST request to `/v3/oauth/token`
to generate a new access token.
4. Save the new access token and also use it with the `Authorization` header.
"""

requires_response_body = True

def __init__(self, base_url: str, secret_key: str):
def __init__(self, base_url: str, credentials: ClientCredentials):
self._base_url = base_url
self._secret_key = secret_key
self._credentials = credentials
self._access_token: Optional[str] = None
self._expires_at: Optional[datetime.datetime] = None

Expand All @@ -36,7 +37,7 @@ def auth_flow(
if (
self._access_token is None
or self._expires_at is None
or self._expires_at < datetime.datetime.now(datetime.timezone.utc)
or self._expires_at < datetime.datetime.now()
):
access_token_response = yield self._build_access_token_request()
self._update_access_token(access_token_response)
Expand All @@ -46,24 +47,33 @@ def auth_flow(
"Something went wrong, there is no access token available."
)

request.headers[AUTHORIZATION] = self._access_token
request.headers[AUTHORIZATION] = f"Bearer {self._access_token}"
response = yield request

if response.status_code == httpx.codes.UNAUTHORIZED:
access_token_response = yield self._build_access_token_request()
self._update_access_token(access_token_response)

request.headers[AUTHORIZATION] = self._access_token
request.headers[AUTHORIZATION] = f"Bearer {self._access_token}"
yield request

def _build_access_token_request(self):
return httpx.Request(
"POST",
f"{self._base_url}/api/v1/access_token/",
json={"secret_key": self._secret_key},
f"{self._base_url}/v3/oauth/token",
json={
"grant_type": "client_credentials",
"vendor_id": self._credentials.vendor_id,
"audience": self._credentials.audience,
"client_id": self._credentials.client_id,
"client_secret": self._credentials.client_secret,
"scopes": self._credentials.scopes,
},
)

def _update_access_token(self, response: httpx.Response):
data = response_utils.get_data_dict(response)
self._access_token = data["access_token"]
self._expires_at = datetime.datetime.fromisoformat(data["expiration_utc"])
self._expires_at = datetime.datetime.now() + datetime.timedelta(
seconds=data["expires_in"]
)
Loading
Loading