Merge branch 'release/v0.1.0'
This commit is contained in:
commit
2da9d56ddf
@ -0,0 +1,5 @@
|
|||||||
|
WHEREIS_API_BASE_URL=https://api-whereis.thegux.fr
|
||||||
|
WHEREIS_API_EMAIL=
|
||||||
|
WHEREIS_API_PASSWORD=
|
||||||
|
|
||||||
|
WHEREIS_CERT_VERIFY=true
|
||||||
2
Makefile
2
Makefile
@ -22,4 +22,4 @@ build: check
|
|||||||
$(PYTHON) -m hatch -v build -t wheel
|
$(PYTHON) -m hatch -v build -t wheel
|
||||||
|
|
||||||
publish: build
|
publish: build
|
||||||
$(PYTHON) -m twine upload --repository gitea dist/*.whl
|
$(PYTHON) -m twine upload --repository whereis-client dist/*.whl
|
||||||
33
README.md
Normal file
33
README.md
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
# whereis-client
|
||||||
|
|
||||||
|
A simple Python client library to interact easyly with the WhereIs REST API.
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install --index-url https://gitea.thegux.fr/api/packages/rmanach/pypi/simple/ whereis-client==<version> --extra-index-url https://pypi.org/simple
|
||||||
|
```
|
||||||
|
|
||||||
|
## How to ?
|
||||||
|
In order to use the client you need to provide some environments variables defined in `.env.example`. You can either:
|
||||||
|
- Copy the `.env.example` into `.env` and feed the variables (`.env` should be next to your main script using the lib)
|
||||||
|
- Add those environments variables to your environment profile.
|
||||||
|
|
||||||
|
Once it's done, you're good to use the client.
|
||||||
|
|
||||||
|
```python
|
||||||
|
from whereis_client import Client
|
||||||
|
|
||||||
|
cli = Client.from_env()
|
||||||
|
lst_gps_positions = cli.get_gps_positions(
|
||||||
|
date_start="2022-12-25", date_end="2022-12-30"
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
For some code samples on how to use the client, take a look at the [main.py](main.py) sample script.
|
||||||
|
|
||||||
|
Enjoy !
|
||||||
|
|
||||||
|
## Contact
|
||||||
|
|
||||||
|
If you have any issues, feel free to contact **admin@thegux.fr** for fixes or upgrades.
|
||||||
113
main.py
113
main.py
@ -1,7 +1,10 @@
|
|||||||
import logging
|
import logging
|
||||||
import sys
|
import sys
|
||||||
|
import json
|
||||||
|
import time
|
||||||
|
|
||||||
from src import VERSION
|
from src import VERSION, Client, OrderField
|
||||||
|
from src.exceptions import WhereIsException, UnauthorizedException
|
||||||
|
|
||||||
|
|
||||||
stdout_handler = logging.StreamHandler(stream=sys.stdout)
|
stdout_handler = logging.StreamHandler(stream=sys.stdout)
|
||||||
@ -12,5 +15,113 @@ logging.basicConfig(
|
|||||||
)
|
)
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
"""
|
||||||
|
This a sample script to demonstrate how to deal with WhereIs API client.
|
||||||
|
|
||||||
|
All you need is to provide some environments variables describe in the `.env.example`.
|
||||||
|
You can:
|
||||||
|
- Copy `.env.example` into `.env` and feed env variables or,
|
||||||
|
- Directly export those mandatories variables
|
||||||
|
|
||||||
|
Once its done, you can initialize the Client:
|
||||||
|
|
||||||
|
cli = Client.from_env()
|
||||||
|
|
||||||
|
and use all the available methods to interact with the WhereIs API.
|
||||||
|
|
||||||
|
Sessions:
|
||||||
|
- create_session
|
||||||
|
- get_sessions
|
||||||
|
- get_session
|
||||||
|
- update_session
|
||||||
|
- delete_session
|
||||||
|
- update_session_users
|
||||||
|
- close_session
|
||||||
|
- watch_session_events
|
||||||
|
- stop_watch_session
|
||||||
|
WS token:
|
||||||
|
- get_wstokens
|
||||||
|
- create_wstoken
|
||||||
|
- delete_wstoken
|
||||||
|
GPS positions:
|
||||||
|
- get_gps_positions
|
||||||
|
"""
|
||||||
logging.info(f"WhereIs client v{VERSION}")
|
logging.info(f"WhereIs client v{VERSION}")
|
||||||
|
|
||||||
|
# initialize the client
|
||||||
|
try:
|
||||||
|
cli = Client.from_env()
|
||||||
|
except WhereIsException as e:
|
||||||
|
logging.error(f"unable to initialize WhereIs API client: {e}")
|
||||||
|
except Exception as e:
|
||||||
|
logging.error("unexpected error occurred while initializing client", exc_info=True)
|
||||||
|
|
||||||
|
# get ws tokens
|
||||||
|
tokens = cli.get_wstokens()
|
||||||
|
print(json.dumps(tokens, indent=2))
|
||||||
|
|
||||||
|
# delete ws token
|
||||||
|
if len(tokens) > 0:
|
||||||
|
token = cli.delete_wstoken(tokens[0].get("id"))
|
||||||
|
print(json.dumps(tokens, indent=2))
|
||||||
|
|
||||||
|
# create ws token
|
||||||
|
try:
|
||||||
|
token = cli.create_wstoken()
|
||||||
|
except WhereIsException as e:
|
||||||
|
logging.error(f"error occurred while creating a ws token, status code: {e.error_code}")
|
||||||
|
print(json.dumps(e.content, indent=2))
|
||||||
|
|
||||||
|
# retrieve all user/public sessions
|
||||||
|
sessions = cli.get_sessions()
|
||||||
|
print(json.dumps(sessions, indent=2))
|
||||||
|
|
||||||
|
# create and update a session (must have `Streamer` role)
|
||||||
|
try:
|
||||||
|
session = cli.create_session(name="RUN-01")
|
||||||
|
session = cli.update_session(session.get("id"), name="RUN-02", description="My second run")
|
||||||
|
except WhereIsException as e:
|
||||||
|
logging.error(f"error occurred while creating/updating a session, status code: {e.error_code}")
|
||||||
|
print(json.dumps(e.content, indent=2))
|
||||||
|
|
||||||
|
# retrieve all user/public sessions with ordering
|
||||||
|
sessions = cli.get_sessions(ordering=OrderField.AscDateStart)
|
||||||
|
print(json.dumps(sessions, indent=2))
|
||||||
|
|
||||||
|
# retrieve all user/public sessions with search
|
||||||
|
sessions = cli.get_sessions(search="run", ordering=OrderField.AscDateStart)
|
||||||
|
print(json.dumps(sessions, indent=2))
|
||||||
|
|
||||||
|
# retrieve session by id
|
||||||
|
session = cli.get_session(session.get("id"))
|
||||||
|
print(json.dumps(session, indent=2))
|
||||||
|
|
||||||
|
# update session users
|
||||||
|
try:
|
||||||
|
session = cli.update_session_users(session.get("id"), [])
|
||||||
|
except WhereIsException as e:
|
||||||
|
logging.error(f"error occurred while updating users session, status code: {e.error_code}")
|
||||||
|
print(json.dumps(e.content, indent=2))
|
||||||
|
|
||||||
|
# close a session
|
||||||
|
try:
|
||||||
|
session = cli.close_session("does-not-exist")
|
||||||
|
except WhereIsException as e:
|
||||||
|
logging.error(f"error occurred while closing session, status code: {e.error_code}")
|
||||||
|
print(json.dumps(e.content, indent=2))
|
||||||
|
|
||||||
|
# get session events
|
||||||
|
cli.watch_session_events(session.get("id"))
|
||||||
|
|
||||||
|
# get gps positions from "2024-12-25T23:00:00" to infinity...
|
||||||
|
gps_positions = cli.get_gps_positions(date_start="2024-12-25T23:00:00")
|
||||||
|
print(json.dumps(gps_positions, indent=2))
|
||||||
|
|
||||||
|
# close the session
|
||||||
|
session = cli.close_session(session.get("id"))
|
||||||
|
|
||||||
|
# delete a session
|
||||||
|
cli.delete_session(session.get("id"))
|
||||||
|
|
||||||
|
# stop session events watcher
|
||||||
|
cli.stop_watch_session(session.get("id"), force=True)
|
||||||
|
|||||||
@ -8,6 +8,8 @@ dynamic = ["version"]
|
|||||||
description = "WhereIs API client library"
|
description = "WhereIs API client library"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"requests==2.32.3",
|
"requests==2.32.3",
|
||||||
|
"python-dotenv==1.0.1",
|
||||||
|
"sseclient-py==1.8.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[tool.hatch.version]
|
[tool.hatch.version]
|
||||||
@ -20,7 +22,7 @@ packages = ["src"]
|
|||||||
only-include = ["src"]
|
only-include = ["src"]
|
||||||
|
|
||||||
[tool.hatch.build.targets.wheel.sources]
|
[tool.hatch.build.targets.wheel.sources]
|
||||||
"src" = "whereis-client"
|
"src" = "whereis_client"
|
||||||
|
|
||||||
[tool.ruff]
|
[tool.ruff]
|
||||||
select = ["E", "F", "I"]
|
select = ["E", "F", "I"]
|
||||||
|
|||||||
@ -1,2 +1,3 @@
|
|||||||
requests==2.32.3
|
requests==2.32.3
|
||||||
python-dotenv==1.0.1
|
python-dotenv==1.0.1
|
||||||
|
sseclient-py==1.8.0
|
||||||
@ -1 +1,5 @@
|
|||||||
|
from .client import Client, OrderField
|
||||||
|
|
||||||
|
__all__ = ["Client", "OrderField"]
|
||||||
|
|
||||||
VERSION = "0.1.0"
|
VERSION = "0.1.0"
|
||||||
|
|||||||
550
src/client.py
Normal file
550
src/client.py
Normal file
@ -0,0 +1,550 @@
|
|||||||
|
import logging
|
||||||
|
import os
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from datetime import datetime as dt
|
||||||
|
from enum import Enum
|
||||||
|
from threading import Event, Thread
|
||||||
|
from typing import Any, Callable
|
||||||
|
from urllib.parse import urljoin
|
||||||
|
from uuid import UUID
|
||||||
|
|
||||||
|
import requests
|
||||||
|
import urllib3
|
||||||
|
from dotenv import dotenv_values
|
||||||
|
from requests import Session
|
||||||
|
from sseclient import SSEClient
|
||||||
|
|
||||||
|
from .exceptions import UnauthorizedException, WhereIsException
|
||||||
|
|
||||||
|
API_DEFAULT_URL = "https://api-whereis.thegux.fr"
|
||||||
|
|
||||||
|
__all__ = ["Client", "OrderField"]
|
||||||
|
|
||||||
|
|
||||||
|
def refresh():
|
||||||
|
"""
|
||||||
|
Catch 401 status code (UnauthorizedException)
|
||||||
|
refresh the access token and retry.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def decorator(func):
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
if len(args) > 0 and isinstance(args[0], Client):
|
||||||
|
for i in range(2):
|
||||||
|
try:
|
||||||
|
return func(*args, **kwargs)
|
||||||
|
except UnauthorizedException as e:
|
||||||
|
if i == 1: # second attempt
|
||||||
|
logging.error(
|
||||||
|
"second call attempt failed after refreshing token"
|
||||||
|
)
|
||||||
|
raise e
|
||||||
|
logging.warning("refresh access token...")
|
||||||
|
args[0]._refresh()
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
|
class OrderField(Enum):
|
||||||
|
"""
|
||||||
|
Ordering query param available fields.
|
||||||
|
|
||||||
|
- AscDateStart: date start ascending order
|
||||||
|
- DesDateStart: date start descending order
|
||||||
|
- AscDateEnd: date end ascending order
|
||||||
|
- DesDateEnd: date end descending order
|
||||||
|
"""
|
||||||
|
|
||||||
|
AscDateStart = "date_start"
|
||||||
|
DesDateStart = "-date_start"
|
||||||
|
AscDateEnd = "date_end"
|
||||||
|
DesDateEnd = "-date_end"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class SessionWatcher:
|
||||||
|
"""
|
||||||
|
Handle the SSE client connection in a daemon thread.
|
||||||
|
|
||||||
|
NOTE: Should not be instanciated directly, use
|
||||||
|
the `Client` instead.
|
||||||
|
"""
|
||||||
|
|
||||||
|
session_id: UUID
|
||||||
|
_thread: Thread
|
||||||
|
_client: SSEClient
|
||||||
|
_event: Event
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_session_id(
|
||||||
|
cls,
|
||||||
|
base_url: str,
|
||||||
|
id_: UUID,
|
||||||
|
headers: dict[str, Any],
|
||||||
|
callback: Callable[[str], None] | None = None,
|
||||||
|
verify: bool = True,
|
||||||
|
) -> "SessionWatcher":
|
||||||
|
session_url = urljoin(base_url, f"/sessions/{id_}/events/")
|
||||||
|
|
||||||
|
headers = {**headers, "Accept": "text/event-stream"}
|
||||||
|
resp = requests.get(session_url, stream=True, headers=headers, verify=verify)
|
||||||
|
client = SSEClient(resp) # type: ignore
|
||||||
|
event = Event()
|
||||||
|
|
||||||
|
def _job():
|
||||||
|
logging.debug(f"SSE client daemon started for session: {id_}")
|
||||||
|
|
||||||
|
for evt in client.events():
|
||||||
|
logging.debug(f"event received for session: {id_}")
|
||||||
|
if callback is not None:
|
||||||
|
callback(evt.data)
|
||||||
|
|
||||||
|
if event.is_set():
|
||||||
|
logging.debug(f"SSE client daemon stopped for session: {id_}")
|
||||||
|
break
|
||||||
|
|
||||||
|
t = Thread(target=_job, daemon=True)
|
||||||
|
t.start()
|
||||||
|
|
||||||
|
return SessionWatcher(id_, t, client, event)
|
||||||
|
|
||||||
|
def stop(self, force: bool = False):
|
||||||
|
"""
|
||||||
|
Send an event to stop the events stream and wait
|
||||||
|
for the thread to finish.
|
||||||
|
|
||||||
|
If you want to stop the stream savagely, you can use
|
||||||
|
the optional arg: `force`.
|
||||||
|
"""
|
||||||
|
self._event.set()
|
||||||
|
self._thread.join(timeout=0 if force else None)
|
||||||
|
logging.debug(f"SSE stream client for session: {self.session_id} closed")
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True)
|
||||||
|
class Client:
|
||||||
|
"""
|
||||||
|
WhereIs API Client main class.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
--------
|
||||||
|
cli = Client.from_env()
|
||||||
|
sessions = cli.get_sessions()
|
||||||
|
"""
|
||||||
|
|
||||||
|
base_url: str
|
||||||
|
email: str
|
||||||
|
password: str
|
||||||
|
|
||||||
|
session: Session = field(init=False)
|
||||||
|
|
||||||
|
sessions_watcher: dict[UUID, SessionWatcher] = field(
|
||||||
|
default_factory=dict, init=False
|
||||||
|
)
|
||||||
|
|
||||||
|
def _login(self) -> WhereIsException | None:
|
||||||
|
"""Get the access token and store it in the `Session` header"""
|
||||||
|
data = {
|
||||||
|
"email": self.email,
|
||||||
|
"password": self.password,
|
||||||
|
}
|
||||||
|
|
||||||
|
login_url = urljoin(self.base_url, "/auth/token/")
|
||||||
|
logging.info(f"login: {login_url}")
|
||||||
|
|
||||||
|
res = self.session.post(login_url, json=data)
|
||||||
|
if res.status_code >= 400:
|
||||||
|
raise WhereIsException(login_url, res)
|
||||||
|
|
||||||
|
access_token = res.json()["access"]
|
||||||
|
self.session.headers.update({"Authorization": f"Bearer {access_token}"})
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _refresh(self) -> WhereIsException | None:
|
||||||
|
"""Refresh the access token and store it in the `Session` header"""
|
||||||
|
refresh_url = urljoin(self.base_url, "/auth/refresh/")
|
||||||
|
logging.info(f"refresh: {refresh_url}")
|
||||||
|
|
||||||
|
res = self.session.post(refresh_url)
|
||||||
|
if res.status_code >= 400:
|
||||||
|
raise WhereIsException(refresh_url, res)
|
||||||
|
|
||||||
|
access_token = res.json()["access"]
|
||||||
|
self.session.headers.update({"Authorization": f"Bearer {access_token}"})
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_env(cls) -> "Client":
|
||||||
|
"""
|
||||||
|
Initialize the client from env variables (.env & global) and
|
||||||
|
log in into the application.
|
||||||
|
|
||||||
|
If the login fails, an exception is raised.
|
||||||
|
"""
|
||||||
|
env_data = {
|
||||||
|
"WHEREIS_API_EMAIL": os.getenv("WHEREIS_API_EMAIL", ""),
|
||||||
|
"WHEREIS_API_PASSWORD": os.getenv("WHEREIS_API_PASSWORD", ""),
|
||||||
|
"WHEREIS_API_BASE_URL": os.getenv("WHEREIS_API_BASE_URL", API_DEFAULT_URL),
|
||||||
|
}
|
||||||
|
dotenv_data = dotenv_values()
|
||||||
|
env_data.update(dotenv_data) # type: ignore
|
||||||
|
|
||||||
|
cli = Client(
|
||||||
|
env_data.get("WHEREIS_API_BASE_URL", ""),
|
||||||
|
env_data.get("WHEREIS_API_EMAIL", ""),
|
||||||
|
env_data.get("WHEREIS_API_PASSWORD", ""),
|
||||||
|
)
|
||||||
|
cli.session = Session()
|
||||||
|
cli.session.headers.update({"content-type": "application/json"})
|
||||||
|
|
||||||
|
is_cert_verify = env_data.get("WHEREIS_CERT_VERIFY", "") != "false"
|
||||||
|
if not is_cert_verify:
|
||||||
|
urllib3.disable_warnings()
|
||||||
|
|
||||||
|
cli.session.verify = is_cert_verify
|
||||||
|
|
||||||
|
cli._login()
|
||||||
|
|
||||||
|
logging.info(f"client successfully initialized for user: {cli.email}")
|
||||||
|
return cli
|
||||||
|
|
||||||
|
@refresh()
|
||||||
|
def _get_sessions_page(self, url: str) -> dict[str, Any] | WhereIsException:
|
||||||
|
"""Get paginate sessions."""
|
||||||
|
res = self.session.get(url)
|
||||||
|
if res.status_code == 401:
|
||||||
|
raise UnauthorizedException(url, res)
|
||||||
|
|
||||||
|
if res.status_code >= 400:
|
||||||
|
raise WhereIsException(url, res)
|
||||||
|
|
||||||
|
return res.json()
|
||||||
|
|
||||||
|
def get_sessions(
|
||||||
|
self, search: str | None = None, ordering: OrderField | None = None
|
||||||
|
) -> list[dict[str, Any]] | WhereIsException:
|
||||||
|
"""
|
||||||
|
Get all user and public sessions.
|
||||||
|
If an error occurred duning the API call an exception is raised.
|
||||||
|
|
||||||
|
Params:
|
||||||
|
-------
|
||||||
|
search: str, search sessions over username and description
|
||||||
|
ordering: OrderField, ordering sessions by dates
|
||||||
|
"""
|
||||||
|
sessions_url = urljoin(self.base_url, "/sessions/")
|
||||||
|
|
||||||
|
has_query_param = False
|
||||||
|
if search is not None:
|
||||||
|
if not has_query_param:
|
||||||
|
sessions_url += "?"
|
||||||
|
has_query_param = True
|
||||||
|
sessions_url += f"search={search}"
|
||||||
|
|
||||||
|
if ordering is not None:
|
||||||
|
if not has_query_param:
|
||||||
|
sessions_url += "?"
|
||||||
|
else:
|
||||||
|
sessions_url += "&"
|
||||||
|
sessions_url += f"ordering={ordering.value}"
|
||||||
|
|
||||||
|
logging.info(f"get sessions: {sessions_url}")
|
||||||
|
|
||||||
|
lst_sessions = []
|
||||||
|
while sessions_url is not None:
|
||||||
|
data = self._get_sessions_page(sessions_url)
|
||||||
|
|
||||||
|
sessions_url = data.get("next")
|
||||||
|
lst_sessions.extend(data.get("results", []))
|
||||||
|
|
||||||
|
return lst_sessions
|
||||||
|
|
||||||
|
@refresh()
|
||||||
|
def get_session(self, id_: UUID) -> list[dict[str, Any]] | WhereIsException:
|
||||||
|
session_url = urljoin(self.base_url, f"/sessions/{id_}/")
|
||||||
|
logging.info(f"get session: {session_url}")
|
||||||
|
|
||||||
|
res = self.session.get(session_url)
|
||||||
|
if res.status_code == 401:
|
||||||
|
raise UnauthorizedException(session_url, res)
|
||||||
|
|
||||||
|
if res.status_code >= 400:
|
||||||
|
raise WhereIsException(session_url, res)
|
||||||
|
|
||||||
|
return res.json()
|
||||||
|
|
||||||
|
@refresh()
|
||||||
|
def create_session(
|
||||||
|
self, name: str, description: str | None = None, is_public: bool = False
|
||||||
|
) -> dict[str, Any] | WhereIsException:
|
||||||
|
sessions_url = urljoin(self.base_url, "/sessions/")
|
||||||
|
logging.info(f"create session: {sessions_url}")
|
||||||
|
|
||||||
|
data = {"name": name, "description": description, "is_public": is_public}
|
||||||
|
if name is None:
|
||||||
|
data.pop("name")
|
||||||
|
if description is None:
|
||||||
|
data.pop("description")
|
||||||
|
if is_public is None:
|
||||||
|
data.pop("is_public")
|
||||||
|
|
||||||
|
res = self.session.post(
|
||||||
|
sessions_url,
|
||||||
|
json=data,
|
||||||
|
)
|
||||||
|
|
||||||
|
if res.status_code == 401:
|
||||||
|
raise UnauthorizedException(sessions_url, res)
|
||||||
|
|
||||||
|
if res.status_code >= 400:
|
||||||
|
raise WhereIsException(sessions_url, res)
|
||||||
|
|
||||||
|
return res.json()
|
||||||
|
|
||||||
|
@refresh()
|
||||||
|
def update_session(
|
||||||
|
self,
|
||||||
|
id_: UUID,
|
||||||
|
name: str | None = None,
|
||||||
|
description: str | None = None,
|
||||||
|
is_public: bool | None = None,
|
||||||
|
) -> dict[str, Any] | WhereIsException:
|
||||||
|
session_url = urljoin(self.base_url, f"/sessions/{id_}/")
|
||||||
|
logging.info(f"update session: {session_url}")
|
||||||
|
|
||||||
|
data = {"name": name, "description": description, "is_public": is_public}
|
||||||
|
if name is None:
|
||||||
|
data.pop("name")
|
||||||
|
if description is None:
|
||||||
|
data.pop("description")
|
||||||
|
if is_public is None:
|
||||||
|
data.pop("is_public")
|
||||||
|
|
||||||
|
res = self.session.patch(session_url, json=data)
|
||||||
|
|
||||||
|
if res.status_code == 401:
|
||||||
|
raise UnauthorizedException(session_url, res)
|
||||||
|
|
||||||
|
if res.status_code >= 400:
|
||||||
|
raise WhereIsException(session_url, res)
|
||||||
|
|
||||||
|
return res.json()
|
||||||
|
|
||||||
|
@refresh()
|
||||||
|
def delete_session(self, id_: UUID) -> None | WhereIsException:
|
||||||
|
"""
|
||||||
|
Close and delete the session.
|
||||||
|
|
||||||
|
NOTE: The GPS positions associated to the session are not deleted !
|
||||||
|
"""
|
||||||
|
session_url = urljoin(self.base_url, f"/sessions/{id_}/")
|
||||||
|
logging.info(f"delete session: {session_url}")
|
||||||
|
|
||||||
|
res = self.session.delete(session_url)
|
||||||
|
|
||||||
|
if res.status_code == 401:
|
||||||
|
raise UnauthorizedException(session_url, res)
|
||||||
|
|
||||||
|
if res.status_code >= 400:
|
||||||
|
raise WhereIsException(session_url, res)
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
@refresh()
|
||||||
|
def update_session_users(
|
||||||
|
self, id_: UUID, users: list[UUID]
|
||||||
|
) -> dict[str, Any] | WhereIsException:
|
||||||
|
"""
|
||||||
|
Update users sessions.
|
||||||
|
|
||||||
|
WARN: An empty users list parameter cleans all users.
|
||||||
|
"""
|
||||||
|
session_url = urljoin(self.base_url, f"/sessions/{id_}/users/")
|
||||||
|
logging.info(f"update session users: {session_url}")
|
||||||
|
|
||||||
|
res = self.session.post(session_url, json={"users": users})
|
||||||
|
|
||||||
|
if res.status_code == 401:
|
||||||
|
raise UnauthorizedException(session_url, res)
|
||||||
|
|
||||||
|
if res.status_code >= 400:
|
||||||
|
raise WhereIsException(session_url, res)
|
||||||
|
|
||||||
|
return res.json()
|
||||||
|
|
||||||
|
@refresh()
|
||||||
|
def close_session(self, id_: UUID) -> dict[str, Any] | WhereIsException:
|
||||||
|
"""Close the DEFINITIVELY the session. Users can't be added anymore."""
|
||||||
|
session_url = urljoin(self.base_url, f"/sessions/{id_}/close/")
|
||||||
|
logging.info(f"close session: {session_url}")
|
||||||
|
|
||||||
|
res = self.session.post(session_url)
|
||||||
|
|
||||||
|
if res.status_code == 401:
|
||||||
|
raise UnauthorizedException(session_url, res)
|
||||||
|
|
||||||
|
if res.status_code >= 400:
|
||||||
|
raise WhereIsException(session_url, res)
|
||||||
|
|
||||||
|
return res.json()
|
||||||
|
|
||||||
|
def watch_session_events(
|
||||||
|
self, id_: UUID, callback: Callable[[str], None] | None = None
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Watch session events through an SSE client.
|
||||||
|
|
||||||
|
It will launch a daemon thread, listening for incoming events.
|
||||||
|
You can use the `callback` optional argument to pass a callable
|
||||||
|
to deal with the events.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
--------
|
||||||
|
def treat_events(evt: str):
|
||||||
|
# your instructions
|
||||||
|
print(evt)
|
||||||
|
|
||||||
|
cli.watch_session_events("session-id", treat_events)
|
||||||
|
|
||||||
|
NOTE: You have to manually manage the connection error (IO, Authentication, etc...)
|
||||||
|
For authentication error, you'll receive this kind of event:
|
||||||
|
{
|
||||||
|
"condition": "forbidden",
|
||||||
|
"text": "Permission denied to channels: session_session-id",
|
||||||
|
"channels": ["session_session-id"]
|
||||||
|
}
|
||||||
|
""" # noqa
|
||||||
|
if self.sessions_watcher.get(id_) is not None:
|
||||||
|
logging.warning(f"you're already watching session events: {id_}")
|
||||||
|
return
|
||||||
|
|
||||||
|
sw = SessionWatcher.from_session_id(
|
||||||
|
self.base_url,
|
||||||
|
id_,
|
||||||
|
self.session.headers, # type: ignore
|
||||||
|
callback,
|
||||||
|
self.session.verify, # type: ignore
|
||||||
|
)
|
||||||
|
|
||||||
|
logging.info(f"session events (id: {id_}) watcher started")
|
||||||
|
self.sessions_watcher[id_] = sw
|
||||||
|
|
||||||
|
def stop_watch_session(self, id_: UUID, force: bool = False):
|
||||||
|
"""
|
||||||
|
Stop watching events for a session.
|
||||||
|
|
||||||
|
Use `force` optional argument to kill the watcher
|
||||||
|
instead of waiting for a graceful stop.
|
||||||
|
"""
|
||||||
|
if (sw := self.sessions_watcher.get(id_)) is not None:
|
||||||
|
sw.stop(force)
|
||||||
|
del self.sessions_watcher[id_]
|
||||||
|
logging.info(f"session events (id: {id_}) watcher stopped")
|
||||||
|
|
||||||
|
@refresh()
|
||||||
|
def get_wstokens(self) -> list[dict[str, Any]] | WhereIsException:
|
||||||
|
wstoken_url = urljoin(self.base_url, "/auth/ws-token/")
|
||||||
|
logging.info(f"get ws token: {wstoken_url}")
|
||||||
|
|
||||||
|
res = self.session.get(wstoken_url)
|
||||||
|
|
||||||
|
if res.status_code == 401:
|
||||||
|
raise UnauthorizedException(wstoken_url, res)
|
||||||
|
|
||||||
|
if res.status_code >= 400:
|
||||||
|
raise WhereIsException(wstoken_url, res)
|
||||||
|
|
||||||
|
return res.json()
|
||||||
|
|
||||||
|
@refresh()
|
||||||
|
def create_wstoken(self) -> dict[str, Any] | WhereIsException:
|
||||||
|
"""
|
||||||
|
Create a websocket JWT to authenticate your real time connection.
|
||||||
|
|
||||||
|
NOTE: only one, and only one ws token per user is allowed.
|
||||||
|
If it expired, delete it and create a new one.
|
||||||
|
"""
|
||||||
|
wstoken_url = urljoin(self.base_url, "/auth/ws-token/")
|
||||||
|
logging.info(f"create ws token: {wstoken_url}")
|
||||||
|
|
||||||
|
res = self.session.post(wstoken_url)
|
||||||
|
|
||||||
|
if res.status_code == 401:
|
||||||
|
raise UnauthorizedException(wstoken_url, res)
|
||||||
|
|
||||||
|
if res.status_code >= 400:
|
||||||
|
raise WhereIsException(wstoken_url, res)
|
||||||
|
|
||||||
|
return res.json()
|
||||||
|
|
||||||
|
@refresh()
|
||||||
|
def delete_wstoken(self, id_: UUID) -> None | WhereIsException:
|
||||||
|
wstoken_url = urljoin(self.base_url, f"/auth/ws-token/{id_}/")
|
||||||
|
logging.info(f"delete ws token: {wstoken_url}")
|
||||||
|
|
||||||
|
res = self.session.delete(wstoken_url)
|
||||||
|
|
||||||
|
if res.status_code == 401:
|
||||||
|
raise UnauthorizedException(wstoken_url, res)
|
||||||
|
|
||||||
|
if res.status_code >= 400:
|
||||||
|
raise WhereIsException(wstoken_url, res)
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
@refresh()
|
||||||
|
def _get_paginate_gps_positions(
|
||||||
|
self, url: str
|
||||||
|
) -> dict[str, Any] | WhereIsException:
|
||||||
|
res = self.session.get(url)
|
||||||
|
|
||||||
|
if res.status_code == 401:
|
||||||
|
raise UnauthorizedException(url, res)
|
||||||
|
|
||||||
|
if res.status_code >= 400:
|
||||||
|
raise WhereIsException(url, res)
|
||||||
|
|
||||||
|
return res.json()
|
||||||
|
|
||||||
|
def get_gps_positions(
|
||||||
|
self,
|
||||||
|
date_start: str | None = None,
|
||||||
|
date_end: str | None = None,
|
||||||
|
) -> list[dict[str, Any]] | ValueError | WhereIsException:
|
||||||
|
"""
|
||||||
|
Gets GPS positions data.
|
||||||
|
|
||||||
|
You can get GPS positions filtered by date interval using
|
||||||
|
optionals arguments `date_start` and `date_end`:
|
||||||
|
- `date_start`: [date_start,]
|
||||||
|
- `date_end`: [date_end,]
|
||||||
|
- `date_start` & `date_end`: [date_start,date_end]
|
||||||
|
|
||||||
|
The dates formats must be in any valid ISO 8601 format otherwise
|
||||||
|
it will raise a ValueError.
|
||||||
|
"""
|
||||||
|
gps_url = urljoin(self.base_url, "/gps/positions/")
|
||||||
|
lst_gps_positions: list[dict[str, Any]] = []
|
||||||
|
|
||||||
|
if date_start:
|
||||||
|
ds = dt.fromisoformat(date_start)
|
||||||
|
gps_url += f"?date_start={ds.isoformat()}"
|
||||||
|
if date_end:
|
||||||
|
de = dt.fromisoformat(date_end)
|
||||||
|
if date_start:
|
||||||
|
gps_url += f"&date_end={de.isoformat()}"
|
||||||
|
else:
|
||||||
|
gps_url += f"?date_end={de.isoformat()}"
|
||||||
|
|
||||||
|
while gps_url is not None:
|
||||||
|
logging.info(f"get gps data from: {gps_url}")
|
||||||
|
|
||||||
|
data = self._get_paginate_gps_positions(gps_url)
|
||||||
|
lst_gps_positions.extend([d for d in data["results"]])
|
||||||
|
gps_url = data["next"]
|
||||||
|
|
||||||
|
return lst_gps_positions
|
||||||
23
src/exceptions.py
Normal file
23
src/exceptions.py
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
from requests import Response
|
||||||
|
|
||||||
|
__all__ = ["WhereIsException", "UnauthorizedException"]
|
||||||
|
|
||||||
|
|
||||||
|
class WhereIsException(Exception):
|
||||||
|
"""Handle all WhereIs API errors."""
|
||||||
|
|
||||||
|
def __init__(self, url: str, response: Response):
|
||||||
|
self.url = url
|
||||||
|
try:
|
||||||
|
self.content = response.json()
|
||||||
|
except Exception:
|
||||||
|
self.content = response.content.decode()
|
||||||
|
super().__init__(self.content)
|
||||||
|
self.error_code = response.status_code
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"error calling: {self.url} - {self.error_code} - {self.content}"
|
||||||
|
|
||||||
|
|
||||||
|
class UnauthorizedException(WhereIsException):
|
||||||
|
pass
|
||||||
Loading…
x
Reference in New Issue
Block a user