|  |  |  | @ -5,6 +5,7 @@ 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 | 
		
	
	
		
			
				
					
					|  |  |  | @ -15,7 +16,7 @@ from sseclient import SSEClient | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  | from .exceptions import UnauthorizedException, WhereIsException | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  | API_DEFAULT_URL = "https://api.locame.duckdns.org" | 
		
	
		
			
				|  |  |  |  | API_DEFAULT_URL = "https://api-whereis.thegux.fr" | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  | __all__ = ["Client", "OrderField"] | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
	
		
			
				
					
					|  |  |  | @ -85,7 +86,7 @@ class SessionWatcher: | 
		
	
		
			
				|  |  |  |  |         callback: Callable[[str], None] | None = None, | 
		
	
		
			
				|  |  |  |  |         verify: bool = True, | 
		
	
		
			
				|  |  |  |  |     ) -> "SessionWatcher": | 
		
	
		
			
				|  |  |  |  |         session_url = base_url + f"/sessions/{id_}/events/" | 
		
	
		
			
				|  |  |  |  |         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) | 
		
	
	
		
			
				
					
					|  |  |  | @ -133,7 +134,7 @@ class Client: | 
		
	
		
			
				|  |  |  |  |     sessions = cli.get_sessions() | 
		
	
		
			
				|  |  |  |  |     """ | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  |     _base_url: str | 
		
	
		
			
				|  |  |  |  |     base_url: str | 
		
	
		
			
				|  |  |  |  |     email: str | 
		
	
		
			
				|  |  |  |  |     password: str | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
	
		
			
				
					
					|  |  |  | @ -143,10 +144,6 @@ class Client: | 
		
	
		
			
				|  |  |  |  |         default_factory=dict, init=False | 
		
	
		
			
				|  |  |  |  |     ) | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  |     @property | 
		
	
		
			
				|  |  |  |  |     def base_url(self) -> str: | 
		
	
		
			
				|  |  |  |  |         return self._base_url.removesuffix("/") | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  |     def _login(self) -> WhereIsException | None: | 
		
	
		
			
				|  |  |  |  |         """Get the access token and store it in the `Session` header""" | 
		
	
		
			
				|  |  |  |  |         data = { | 
		
	
	
		
			
				
					
					|  |  |  | @ -154,7 +151,7 @@ class Client: | 
		
	
		
			
				|  |  |  |  |             "password": self.password, | 
		
	
		
			
				|  |  |  |  |         } | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  |         login_url = self.base_url + "/auth/token/" | 
		
	
		
			
				|  |  |  |  |         login_url = urljoin(self.base_url, "/auth/token/") | 
		
	
		
			
				|  |  |  |  |         logging.info(f"login: {login_url}") | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  |         res = self.session.post(login_url, json=data) | 
		
	
	
		
			
				
					
					|  |  |  | @ -168,7 +165,7 @@ class Client: | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  |     def _refresh(self) -> WhereIsException | None: | 
		
	
		
			
				|  |  |  |  |         """Refresh the access token and store it in the `Session` header""" | 
		
	
		
			
				|  |  |  |  |         refresh_url = self.base_url + "/auth/refresh/" | 
		
	
		
			
				|  |  |  |  |         refresh_url = urljoin(self.base_url, "/auth/refresh/") | 
		
	
		
			
				|  |  |  |  |         logging.info(f"refresh: {refresh_url}") | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  |         res = self.session.post(refresh_url) | 
		
	
	
		
			
				
					
					|  |  |  | @ -180,24 +177,6 @@ class Client: | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  |         return None | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  |     @staticmethod | 
		
	
		
			
				|  |  |  |  |     def _init_client( | 
		
	
		
			
				|  |  |  |  |         base_url: str, email: str, password: str, verify: bool = True | 
		
	
		
			
				|  |  |  |  |     ) -> "Client": | 
		
	
		
			
				|  |  |  |  |         cli = Client(base_url, email, password) | 
		
	
		
			
				|  |  |  |  |         cli.session = Session() | 
		
	
		
			
				|  |  |  |  |         cli.session.headers.update({"content-type": "application/json"}) | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  |         if not verify: | 
		
	
		
			
				|  |  |  |  |             urllib3.disable_warnings() | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  |         cli.session.verify = verify | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  |         cli._login() | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  |         logging.info(f"client successfully initialized for user: {cli.email}") | 
		
	
		
			
				|  |  |  |  |         return cli | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  |     @classmethod | 
		
	
		
			
				|  |  |  |  |     def from_env(cls) -> "Client": | 
		
	
		
			
				|  |  |  |  |         """ | 
		
	
	
		
			
				
					
					|  |  |  | @ -214,17 +193,24 @@ class Client: | 
		
	
		
			
				|  |  |  |  |         dotenv_data = dotenv_values() | 
		
	
		
			
				|  |  |  |  |         env_data.update(dotenv_data)  # type: ignore | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  |         return Client._init_client( | 
		
	
		
			
				|  |  |  |  |         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"}) | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  |     @classmethod | 
		
	
		
			
				|  |  |  |  |     def from_creds( | 
		
	
		
			
				|  |  |  |  |         cls, base_url: str, email: str, password: str, verify: bool = False | 
		
	
		
			
				|  |  |  |  |     ) -> "Client": | 
		
	
		
			
				|  |  |  |  |         return Client._init_client(base_url, email, password, verify) | 
		
	
		
			
				|  |  |  |  |         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: | 
		
	
	
		
			
				
					
					|  |  |  | @ -250,7 +236,7 @@ class Client: | 
		
	
		
			
				|  |  |  |  |         search: str, search sessions over username and description | 
		
	
		
			
				|  |  |  |  |         ordering: OrderField, ordering sessions by dates | 
		
	
		
			
				|  |  |  |  |         """ | 
		
	
		
			
				|  |  |  |  |         sessions_url = self.base_url + "/sessions/" | 
		
	
		
			
				|  |  |  |  |         sessions_url = urljoin(self.base_url, "/sessions/") | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  |         has_query_param = False | 
		
	
		
			
				|  |  |  |  |         if search is not None: | 
		
	
	
		
			
				
					
					|  |  |  | @ -279,7 +265,7 @@ class Client: | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  |     @refresh() | 
		
	
		
			
				|  |  |  |  |     def get_session(self, id_: UUID) -> list[dict[str, Any]] | WhereIsException: | 
		
	
		
			
				|  |  |  |  |         session_url = self.base_url + f"/sessions/{id_}/" | 
		
	
		
			
				|  |  |  |  |         session_url = urljoin(self.base_url, f"/sessions/{id_}/") | 
		
	
		
			
				|  |  |  |  |         logging.info(f"get session: {session_url}") | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  |         res = self.session.get(session_url) | 
		
	
	
		
			
				
					
					|  |  |  | @ -295,7 +281,7 @@ class Client: | 
		
	
		
			
				|  |  |  |  |     def create_session( | 
		
	
		
			
				|  |  |  |  |         self, name: str, description: str | None = None, is_public: bool = False | 
		
	
		
			
				|  |  |  |  |     ) -> dict[str, Any] | WhereIsException: | 
		
	
		
			
				|  |  |  |  |         sessions_url = self.base_url + "/sessions/" | 
		
	
		
			
				|  |  |  |  |         sessions_url = urljoin(self.base_url, "/sessions/") | 
		
	
		
			
				|  |  |  |  |         logging.info(f"create session: {sessions_url}") | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  |         data = {"name": name, "description": description, "is_public": is_public} | 
		
	
	
		
			
				
					
					|  |  |  | @ -327,7 +313,7 @@ class Client: | 
		
	
		
			
				|  |  |  |  |         description: str | None = None, | 
		
	
		
			
				|  |  |  |  |         is_public: bool | None = None, | 
		
	
		
			
				|  |  |  |  |     ) -> dict[str, Any] | WhereIsException: | 
		
	
		
			
				|  |  |  |  |         session_url = self.base_url + f"/sessions/{id_}/" | 
		
	
		
			
				|  |  |  |  |         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} | 
		
	
	
		
			
				
					
					|  |  |  | @ -355,7 +341,7 @@ class Client: | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  |         NOTE: The GPS positions associated to the session are not deleted ! | 
		
	
		
			
				|  |  |  |  |         """ | 
		
	
		
			
				|  |  |  |  |         session_url = self.base_url + f"/sessions/{id_}/" | 
		
	
		
			
				|  |  |  |  |         session_url = urljoin(self.base_url, f"/sessions/{id_}/") | 
		
	
		
			
				|  |  |  |  |         logging.info(f"delete session: {session_url}") | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  |         res = self.session.delete(session_url) | 
		
	
	
		
			
				
					
					|  |  |  | @ -377,7 +363,7 @@ class Client: | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  |         WARN: An empty users list parameter cleans all users. | 
		
	
		
			
				|  |  |  |  |         """ | 
		
	
		
			
				|  |  |  |  |         session_url = self.base_url + f"/sessions/{id_}/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}) | 
		
	
	
		
			
				
					
					|  |  |  | @ -393,7 +379,7 @@ class Client: | 
		
	
		
			
				|  |  |  |  |     @refresh() | 
		
	
		
			
				|  |  |  |  |     def close_session(self, id_: UUID) -> dict[str, Any] | WhereIsException: | 
		
	
		
			
				|  |  |  |  |         """Close the DEFINITIVELY the session. Users can't be added anymore.""" | 
		
	
		
			
				|  |  |  |  |         session_url = self.base_url + f"/sessions/{id_}/close/" | 
		
	
		
			
				|  |  |  |  |         session_url = urljoin(self.base_url, f"/sessions/{id_}/close/") | 
		
	
		
			
				|  |  |  |  |         logging.info(f"close session: {session_url}") | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  |         res = self.session.post(session_url) | 
		
	
	
		
			
				
					
					|  |  |  | @ -461,7 +447,7 @@ class Client: | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  |     @refresh() | 
		
	
		
			
				|  |  |  |  |     def get_wstokens(self) -> list[dict[str, Any]] | WhereIsException: | 
		
	
		
			
				|  |  |  |  |         wstoken_url = self.base_url + "/auth/ws-token/" | 
		
	
		
			
				|  |  |  |  |         wstoken_url = urljoin(self.base_url, "/auth/ws-token/") | 
		
	
		
			
				|  |  |  |  |         logging.info(f"get ws token: {wstoken_url}") | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  |         res = self.session.get(wstoken_url) | 
		
	
	
		
			
				
					
					|  |  |  | @ -482,7 +468,7 @@ class Client: | 
		
	
		
			
				|  |  |  |  |         NOTE: only one, and only one ws token per user is allowed. | 
		
	
		
			
				|  |  |  |  |         If it expired, delete it and create a new one. | 
		
	
		
			
				|  |  |  |  |         """ | 
		
	
		
			
				|  |  |  |  |         wstoken_url = self.base_url + "/auth/ws-token/" | 
		
	
		
			
				|  |  |  |  |         wstoken_url = urljoin(self.base_url, "/auth/ws-token/") | 
		
	
		
			
				|  |  |  |  |         logging.info(f"create ws token: {wstoken_url}") | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  |         res = self.session.post(wstoken_url) | 
		
	
	
		
			
				
					
					|  |  |  | @ -497,7 +483,7 @@ class Client: | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  |     @refresh() | 
		
	
		
			
				|  |  |  |  |     def delete_wstoken(self, id_: UUID) -> None | WhereIsException: | 
		
	
		
			
				|  |  |  |  |         wstoken_url = self.base_url + f"/auth/ws-token/{id_}/" | 
		
	
		
			
				|  |  |  |  |         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) | 
		
	
	
		
			
				
					
					|  |  |  | @ -541,7 +527,7 @@ class Client: | 
		
	
		
			
				|  |  |  |  |         The dates formats must be in any valid ISO 8601 format otherwise | 
		
	
		
			
				|  |  |  |  |         it will raise a ValueError. | 
		
	
		
			
				|  |  |  |  |         """ | 
		
	
		
			
				|  |  |  |  |         gps_url = self.base_url + "/gps/positions/" | 
		
	
		
			
				|  |  |  |  |         gps_url = urljoin(self.base_url, "/gps/positions/") | 
		
	
		
			
				|  |  |  |  |         lst_gps_positions: list[dict[str, Any]] = [] | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  |         if date_start: | 
		
	
	
		
			
				
					
					|  |  |  | @ -555,10 +541,6 @@ class Client: | 
		
	
		
			
				|  |  |  |  |                 gps_url += f"?date_end={de.isoformat()}" | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  |         while gps_url is not None: | 
		
	
		
			
				|  |  |  |  |             # pagination api next url returns http scheme instead of https | 
		
	
		
			
				|  |  |  |  |             if self.base_url.startswith("https"): | 
		
	
		
			
				|  |  |  |  |                 gps_url = gps_url.replace("http://", "https://") | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  |             logging.info(f"get gps data from: {gps_url}") | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  |             data = self._get_paginate_gps_positions(gps_url) | 
		
	
	
		
			
				
					
					|  |  |  | 
 |