pax_global_header00006660000000000000000000000064147061734050014521gustar00rootroot0000000000000052 comment=ebed92110997b2e5e8b9439ba3a944d209fa9cf2 python-mypermobil-0.1.8/000077500000000000000000000000001470617340500152255ustar00rootroot00000000000000python-mypermobil-0.1.8/.gitignore000066400000000000000000000001141470617340500172110ustar00rootroot00000000000000dist .vscode build venv mypermobil.egg-info *.pyc tests/.coverage .coverage python-mypermobil-0.1.8/LICENSE000066400000000000000000000020541470617340500162330ustar00rootroot00000000000000MIT License Copyright (c) 2024 Isak Nyberg Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. python-mypermobil-0.1.8/README.md000066400000000000000000000127231470617340500165110ustar00rootroot00000000000000# MyPermobil-API [![PyPI version](https://badge.fury.io/py/mypermobil.svg)](https://badge.fury.io/py/mypermobil) This is a subset of the MyPermobil API, originally published to be used with HomeAssistant. ## Installation The package is available on [pypi.org](https://pypi.org/project/mypermobil/) and installed with the command python -m pip install mypermobil It can also be manually installed by git clone https://github.com/IsakNyberg/mypermobil.git cd mypermobil python -m pip install . ## REST API This API is a REST API that uses JSON as the data format. For most requests, the API requires an authentication token. The token is sent in the header of the request. The token can be created with the code in the example folder. ## Endpoints Supported endpoints are listed in the const.py, custom endpoints are also possible as arguments to the get and post methods. The available endpoints are: GET regions POST applicationlinks POST applicationauthentications GET battery-info GET voiceaccess/dailyusage GET voiceaccess/chargetime GET voiceaccess/chairstatus GET voiceaccess/usagerecords GET /api/v1/products GET /api/v1/products/{product_id} GET /api/v1/products/{product_id}/positions Some endpoints require a product id, this can be found in the `PRODUCTS_ID` item from the by using the `GET ENDPOINT_PRODUCTS` endpoint. This value can be set in the `MyPermobil` class. ## MyPermobil Class The `MyPermobil` class is the main class of the API. The class can store information needed for the requests and can be used to make requests to the API. The MyPermobil class uses `aiohttp` to make requests are they can be made asynchronously. The `create_session` function can be used to create a session without the need to import `aiohttp`. Just remember to close the session when it is no longer needed. The minimum example of the class can be instantiated with: session = create_session() p = MyPermobil("application_name", session) ... p.close_session() To authenticate the app use the `request_application_code()` and `request_application_token()` methods. A detailed example can be found in the example folder. If the app has already been authenticated, the class can be created with the data directly instead. The data will be locally validated with the `self_authenticate()` method. p = MyPermobil( application, session, email, region, code, token, expiration_date, product_id, ) p.self_authenticate() `application`, `session`, `email`, `region`, `token` and `expiration_date` are required to authenticate the app. The `product_id` is optional and can be set later. ## Items Items are lists that describe the path needed to traverse the JSON tree of the corresponding endpoint that is associated with the item. Each association is listed in the `ITEM_LOOKUP` with the reverse lookup in `ENDPOINT_LOOKUP` (however since items are not always unique, the `ENDPOINT_LOOKUP` will only return the first endpoint associated with the item). An example is: PRODUCT_BY_ID_MOST_RECENT_ODOMETER_TOTAL = ["mostRecent", "odometerTotal"] ENDPOINT_PRODUCT_BY_ID = "/api/v1/products/{product_id}" { "_id": "649adb...e377", "WCSerial": "48...28", "BrandId": "567...f50c", "lastUpdated": "2023", "mostRecent": { "odometerTotal": 386944, <----- "odometerTrip": 375795, }, "battery": {...}, "serviceAgreements": [...], "certificates": [...] } In this scenario the `PRODUCT_BY_ID_MOST_RECENT_ODOMETER_TOTAL` item would return the value `386944` from the JSON tree. ### Adding new items Adding a new item can be done in the const.py file. The item should be added next to its endpoint and then in the the `ITEM_LOOKUP` dictionary. The item should be a list of strings or number with each step needed to take in the JSON tree. If the named with the following format: ENDPOINT_ENDPOINT_NAME = "/api/v1/endpointname" ENDPOINTNAME_ITEM_NEW = ["path", "to", "item", 0] The item can then be added to the `ENDPOINT_LOOKUP` dictionary with the following format: ITEM_LOOKUP = { ... ENDPOINT_ENDPOINT_NAME: [ ENDPOINTNAME_ITEM_1, ENDPOINTNAME_ITEM_2, ENDPOINTNAME_ITEM_NEW, <----- ENDPOINTNAME_ITEM_3, ], ... } Doing this will automatically add the item to the `ENDPOINT_LOOKUP` dictionary, if the item is unique. The new item can then be accessed by using the `request_item` method in the `MyPermobil` class. MyPermobil.request_item(ENDPOINTNAME_ITEM_NEW) Or if the item is not unique and the endpoint needs to be specified: MyPermobil.request_item(ENDPOINTNAME_ITEM_NEW, endpoint=ENDPOINT_ENDPOINT_NAME) ### Async All requests are made asynchronously. This means that the requests can be made in parallel. For every methods that uses the `async` keyword it must be awaited. This can be done by using the `asyncio` library. This was done in order to comply with Home Assistant. import asyncio async def main(): await p.request_regions() asyncio.run(main()) ### Caching Many of the requests have the `@cachable` decorator. This decorator will cache the response of the request for 5 minutes. This is to prevent the API from being overloaded with requests, in particular when multiple items to the same endpoint is requested. python-mypermobil-0.1.8/example/000077500000000000000000000000001470617340500166605ustar00rootroot00000000000000python-mypermobil-0.1.8/example/example_auth.py000066400000000000000000000035471470617340500217170ustar00rootroot00000000000000"""Example of how to authenticate a MyPermobil instance""" import asyncio import mypermobil async def main(): """Example of how to authenticate a MyPermobil instance""" # Create a session # mypermobil uses aiohttp.ClientSession for the requests session = await mypermobil.create_session() p_api = mypermobil.MyPermobil( session=session, application="example-application", ) # get email email = input("Email: ") p_api.set_email(email) is_internal = email.endswith("@permobil.com") # request regions region_names = await p_api.request_region_names(include_internal=is_internal) print("Regions:") print(", ".join([region for region in region_names])) region_name = input("Select one: ") while region_name not in region_names: region_name = input("Select one: ") region = region_names[region_name] p_api.set_region(region) # send code request await p_api.request_application_code() print("Check your email for a code") code = input("Code: ") while True: try: p_api.set_code(code) break except mypermobil.MyPermobilClientException as err: print(err) code = input("Code: ") # get token try: token, ttl = await p_api.request_application_token() # íf your instance is linked to a single account set the values p_api.set_token(token) p_api.set_expiration_date(ttl) print("Token:", token) print("Expiration:", ttl) except mypermobil.MyPermobilException as err: print(err) # mark as authenticated p_api.self_authenticate() ################# # # # Do stuff here # # # ################# # close session await p_api.close_session() if __name__ == "__main__": asyncio.run(main()) python-mypermobil-0.1.8/example/example_construct.py000066400000000000000000000021051470617340500227670ustar00rootroot00000000000000"""Example of how to construct a MyPermobil instance""" import asyncio from datetime import datetime, timedelta import mypermobil async def main(): """Example of how to construct a MyPermobil instance""" # Create a session # mypermobil uses aiohttp.ClientSession for the requests session = await mypermobil.create_session() email = "example@email.com" token = "a" * 256 expiration = (datetime.now() + timedelta(days=365)).strftime("%Y-%m-%d") region = "https://region.com/api/v1" application = "example-application" p_api = mypermobil.MyPermobil( application=application, session=session, email=email, token=token, expiration_date=expiration, region=region, ) # mark as authenticated p_api.self_authenticate() ################# # # # Do stuff here # # # ################# # p_api.request_item(mypermobil.BATTERY_STATE_OF_CHARGE) # close session await p_api.close_session() if __name__ == "__main__": asyncio.run(main()) python-mypermobil-0.1.8/example/example_region.py000066400000000000000000000012511470617340500222270ustar00rootroot00000000000000"""Example of how to authenticate a MyPermobil instance""" import asyncio import mypermobil async def main(): """Example of how to authenticate a MyPermobil instance""" # Create a session # mypermobil uses aiohttp.ClientSession for the requests session = await mypermobil.create_session() p_api = mypermobil.MyPermobil( session=session, application="example-application", ) try: regions = await p_api.request_regions(include_internal=True) print("\n".join([regions[region_id].get("url") for region_id in regions])) finally: await p_api.close_session() if __name__ == "__main__": asyncio.run(main()) python-mypermobil-0.1.8/mypermobil/000077500000000000000000000000001470617340500174045ustar00rootroot00000000000000python-mypermobil-0.1.8/mypermobil/__init__.py000066400000000000000000000004241470617340500215150ustar00rootroot00000000000000__version__ = "0.1.8" from mypermobil.mypermobil import ( MyPermobil, create_session, MyPermobilException, MyPermobilAPIException, MyPermobilEulaException, MyPermobilClientException, MyPermobilConnectionException, ) from mypermobil.const import * python-mypermobil-0.1.8/mypermobil/const.py000066400000000000000000000103461470617340500211100ustar00rootroot00000000000000"""Constants for the Permobil integration.""" GET = "get" POST = "post" PUT = "put" DELETE = "delete" EMAIL_REGEX = r"[^@]+@[^@]+\.[^@]+" MILES = "miles" KILOMETERS = "kilometers" GET_REGIONS = "https://cwcprod.permobil.com/api/v1/regions?includeFlags=on" GET_REGIONS_NO_FLAGS = "https://cwcprod.permobil.com/api/v1/regions" ENDPOINT_APPLICATIONLINKS = "/api/v1/users/applicationlinks" ENDPOINT_APPLICATIONAUTHENTICATIONS = "/api/v1/users/applicationauthentications" # Information endpoints ENDPOINT_BATTERY_INFO = "/api/v1/products/battery-info" BATTERY_STATE_OF_HEALTH = ["stateOfHealth"] BATTERY_STATE_OF_CHARGE = ["stateOfCharge"] BATTERY_CHARGING = ["charging"] BATTERY_CHARGE_TIME_LEFT = ["chargeTimeLeft"] BATTERY_DISTANCE_LEFT = ["distanceLeft"] BATTERY_LOCAL_DISTANCE_LEFT = ["localDistanceLeft"] BATTERY_INDOOR_DRIVE_TIME = ["indoorDriveTime"] BATTERY_MAX_INDOOR_DRIVE_TIME = ["maxIndoorDriveTime"] BATTERY_DISTANCE_UNIT = ["distanceUnit"] BATTERY_MAX_AMPERE_HOURS = ["maxAmpereHours"] BATTERY_AMPERE_HOURS_LEFT = ["ampereHoursLeft"] BATTERY_MAX_DISTANCE_LEFT = ["maxDistanceLeft"] BATTERY_TIMESTAMP = ["localTimestamp"] BATTERY_LOCAL_TIMESTAMP = ["localTimestamp"] ENDPOINT_DAILY_USAGE = "/api/v1/products/voiceaccess/dailyusage" USAGE_DISTANCE = ["distance"] USAGE_DISTANCE_UNIT = ["distanceUnit"] USAGE_ADJUSTMENTS = ["adjustments"] # The same info is available in the battery info endpoint ENDPOINT_VA_CHARGE_TIME = "/api/v1/products/voiceaccess/chargetime" CHARGE_TIME_UNKNOWN = ["unknown"] CHARGE_CHARGING_NOW = ["chargingNow"] CHARGE_CHARGE_TIME_LEFT = ["chargeTimeLeft"] CHARGE_CHARGE_TIME_LEFT_MINUTES = ["minutes"] CHARGE_CHARGE_TIME_LEFT_HOURS = ["hours"] ENDPOINT_VA_CHAIR_STATUS = "/api/v1/products/voiceaccess/chairstatus" STATUS_STATUS = ["status"] ENDPOINT_VA_USAGE_RECORDS = "/api/v1/products/voiceaccess/usagerecords" RECORDS_DISTANCE = ["distanceRecord"] RECORDS_DISTANCE_UNIT = ["distanceUnit"] RECORDS_DISTANCE_DATE = ["distanceRecordDate"] RECORDS_SEATING = ["seatingRecord"] RECORDS_SEATING_DATE = ["seatingRecordDate"] ENDPOINT_PRODUCTS = "/api/v1/products" PRODUCTS_ID = [0, "_id"] PRODUCTS_MODEL = [0, "Model"] ENDPOINT_PRODUCT_BY_ID = "/api/v1/products/{product_id}" PRODUCT_BY_ID_UPDATED_AT = ["updatedAt"] PRODUCT_BY_ID_MOST_RECENT = ["mostRecent"] PRODUCT_BY_ID_MOST_RECENT_ODOMETER_TOTAL = [*PRODUCT_BY_ID_MOST_RECENT, "odometerTotal"] PRODUCT_BY_ID_MOST_RECENT_ODOMETER_TRIP = [*PRODUCT_BY_ID_MOST_RECENT, "odometerTrip"] ENDPOINT_PRODUCTS_POSITIONS = "/api/v1/products/{product_id}/positions" POSITIONS_CURRENT = ["currentPosition"] POSITIONS_PREVIOUS = ["previousPositions"] ITEM_LOOKUP = { ENDPOINT_BATTERY_INFO: [ BATTERY_STATE_OF_HEALTH, BATTERY_STATE_OF_CHARGE, BATTERY_CHARGING, BATTERY_CHARGE_TIME_LEFT, BATTERY_DISTANCE_LEFT, BATTERY_LOCAL_DISTANCE_LEFT, BATTERY_INDOOR_DRIVE_TIME, BATTERY_MAX_INDOOR_DRIVE_TIME, BATTERY_DISTANCE_UNIT, BATTERY_MAX_AMPERE_HOURS, BATTERY_AMPERE_HOURS_LEFT, BATTERY_MAX_DISTANCE_LEFT, BATTERY_TIMESTAMP, BATTERY_LOCAL_TIMESTAMP, ], ENDPOINT_DAILY_USAGE: [ USAGE_DISTANCE, USAGE_DISTANCE_UNIT, USAGE_ADJUSTMENTS, ], ENDPOINT_VA_CHARGE_TIME: [ CHARGE_TIME_UNKNOWN, CHARGE_CHARGING_NOW, CHARGE_CHARGE_TIME_LEFT, CHARGE_CHARGE_TIME_LEFT_MINUTES, CHARGE_CHARGE_TIME_LEFT_HOURS, ], ENDPOINT_VA_CHAIR_STATUS: [ STATUS_STATUS, ], ENDPOINT_VA_USAGE_RECORDS: [ RECORDS_DISTANCE, RECORDS_DISTANCE_UNIT, RECORDS_DISTANCE_DATE, RECORDS_SEATING, RECORDS_SEATING_DATE, ], ENDPOINT_PRODUCTS_POSITIONS: [ POSITIONS_CURRENT, POSITIONS_PREVIOUS, ], ENDPOINT_PRODUCTS: [ PRODUCTS_ID, PRODUCTS_MODEL, ], ENDPOINT_PRODUCT_BY_ID: [ PRODUCT_BY_ID_MOST_RECENT, PRODUCT_BY_ID_MOST_RECENT_ODOMETER_TOTAL, PRODUCT_BY_ID_MOST_RECENT_ODOMETER_TRIP, PRODUCT_BY_ID_UPDATED_AT, ], } # when multiple endpoints have the same item, the FIRST one in the list will be used ENDPOINT_LOOKUP = { str(item): endpoint for endpoint, items in list(ITEM_LOOKUP.items())[::-1] for item in items } python-mypermobil-0.1.8/mypermobil/mypermobil.py000066400000000000000000000461171470617340500221460ustar00rootroot00000000000000"""Permobil API.""" import asyncio import datetime import re import aiohttp from aiocache import Cache from .const import ( ENDPOINT_APPLICATIONAUTHENTICATIONS, ENDPOINT_APPLICATIONLINKS, ENDPOINT_LOOKUP, ENDPOINT_PRODUCTS, ENDPOINT_BATTERY_INFO, ENDPOINT_DAILY_USAGE, ENDPOINT_VA_USAGE_RECORDS, ENDPOINT_PRODUCTS_POSITIONS, PRODUCTS_ID, GET_REGIONS, EMAIL_REGEX, GET, POST, PUT, DELETE, ) class MyPermobilException(Exception): """Permobil Exception. Generic Permobil Exception.""" class MyPermobilAPIException(MyPermobilException): """Permobil Exception. Exception raised when the API returns an error.""" class MyPermobilConnectionException(MyPermobilException): """Permobil Exception. Exception raised when the AIOHTTP.""" class MyPermobilClientException(MyPermobilException): """Permobil Exception. Exception raised when the Client is used incorrectly.""" class MyPermobilEulaException(MyPermobilException): """Permobil Exception. Exception raised when the client has not accepted the EULA.""" CACHE: Cache = Cache() CACHE_TTL = 5 * 60 # 5 minutes CACHE_ERROR_TTL = 10 # 10 seconds CACHE_LOCKS = {} async def async_get_cache(key): """Get cache.""" # get the cached data res = await CACHE.get(key) if isinstance(res, Exception): # if the cached data is an error, raise instead of returning raise res return res def cacheable(func): """Decorator to cache function calls for methods that fetch multiple data points from the API. """ async def wrapper(*args, **kwargs): """wrapper.""" key = func.__name__ + str(args) + str(kwargs) # check if the request is already cached cached_data = await async_get_cache(key) if cached_data: # return cached data return cached_data # check if the request is already in progress if key in CACHE_LOCKS: # request is already in progress, wait for it to finish await CACHE_LOCKS[key].wait() res = await async_get_cache(key) return res # return cached data once it has finished by other task # start request it and lock other requests from starting CACHE_LOCKS[key] = asyncio.Event() try: response = await func(*args, **kwargs) # make the request await CACHE.set(key, response, ttl=CACHE_TTL) # cache the response except Exception as err: # pylint: disable=broad-except # if there is an error, cache the error and raise it await CACHE.set(key, err, ttl=CACHE_ERROR_TTL) raise err finally: # regardless of the outcome, unlock other threads CACHE_LOCKS[key].set() del CACHE_LOCKS[key] return response return wrapper def validate_email(email: str) -> str: """Validates an email.""" if not email: raise MyPermobilClientException("Missing email") if not re.match(EMAIL_REGEX, email): raise MyPermobilClientException("Invalid email") return email def validate_code(code: str) -> str: """Validates an code.""" if not code: raise MyPermobilClientException("Missing code") if " " in code or "\n" in code: raise MyPermobilClientException("Code cannot contain spaces or newlines") if not code.isdigit(): raise MyPermobilClientException("Code must be a number") if len(code) != 6: raise MyPermobilClientException("Code must be 6 digits long") return code def validate_token(token: str) -> str: """Validates an token.""" if not token: raise MyPermobilClientException("Missing token") if len(token) != 256: # the token must be 256 characters long raise MyPermobilClientException("Invalid token") return token def validate_expiration_date(expiration_date: str) -> str: """Validates an expiration date, both format and value.""" if not expiration_date: raise MyPermobilClientException("Missing expiration date") date = None try: date = datetime.datetime.strptime(expiration_date, "%Y-%m-%d") except ValueError as err: raise MyPermobilClientException("Invalid expiration date") from err # check if the expiration date is in the future if date < datetime.datetime.now(): raise MyPermobilClientException("Expired token") return expiration_date def validate_region(region: str) -> str: """Validates an region.""" if not region: raise MyPermobilClientException("Missing region") if not region.startswith("https://") and not region.startswith("http://"): raise MyPermobilClientException("Region missing protocol") return region def validate_product_id(product_id: str) -> str: """Validates a product_id.""" if not product_id: raise MyPermobilClientException("Missing product id") if len(product_id) != 24: raise MyPermobilClientException("Invalid product id") return product_id async def create_session(): """Create a client session.""" return aiohttp.ClientSession() class MyPermobil: """Permobil API.""" request_timeout = 10 def __init__( self, application: str, session: aiohttp.ClientSession, email: str = None, region: str = None, code: str = None, token: str = None, expiration_date: str = None, product_id: str = None, ) -> None: """Initialize.""" self.application = application self.session = session self.email = email self.region = region self.code = code self.token = token self.expiration_date = expiration_date self.product_id = product_id self.authenticated = False # Magic methods def __str__(self) -> str: """str.""" app, email, region = self.application, self.email, self.region code, token, exp = self.code, self.token, self.expiration_date return f"Permobil({app}, {email}, {region}, {code}, {token}, {exp})" # Selectors @property def headers(self): """headers.""" if not self.authenticated: raise MyPermobilClientException("Not authenticated") return {"Authorization": f"Bearer {self.token}"} def set_email(self, email: str): """Set email.""" if self.authenticated: raise MyPermobilClientException("Cannot change email after authentication") self.email = validate_email(email) def set_region(self, region: str): """Set region.""" if self.authenticated: raise MyPermobilClientException("Cannot change region after authentication") self.region = validate_region(region) def set_code(self, code: int): """Set code.""" if self.authenticated: raise MyPermobilClientException("Cannot change code after authentication") self.code = validate_code(code) def set_token(self, token: str): """Set token.""" if self.authenticated: raise MyPermobilClientException("Cannot change token after authentication") self.token = validate_token(token) def set_expiration_date(self, expiration_date: str): """Set expiration date.""" if self.authenticated: raise MyPermobilClientException("Cannot change date after authentication") self.expiration_date = validate_expiration_date(expiration_date) def set_product_id(self, product_id: str): """Set product id.""" self.product_id = validate_product_id(product_id) def set_application(self, application: str): """Set application.""" if self.authenticated: raise MyPermobilClientException("Cannot change app after authentication") self.application = application async def close_session(self): """Close session.""" if self.session is None: raise MyPermobilClientException("Session does not exist") await self.session.close() self.session = None def self_authenticate(self): """authenticate. Manually set token and expiration date.""" if self.authenticated: raise MyPermobilClientException("Already authenticated") if not self.application: raise MyPermobilClientException("Missing application name") validate_region(self.region) validate_email(self.email) validate_token(self.token) validate_expiration_date(self.expiration_date) self.authenticated = True def self_reauthenticate(self): """Use when token is expired. Reset token, expiration, and code. Sets authenticated to False. """ if self.authenticated: raise MyPermobilClientException("Already authenticated") if not self.application: raise MyPermobilClientException("Missing application name") self.token = None self.expiration_date = None self.code = None self.authenticated = False # API Methods async def make_request(self, request_type: str, *args, **kwargs): """make a post, get, put or delete request""" if not kwargs.get("timeout"): kwargs["timeout"] = self.request_timeout if not kwargs.get("headers") and self.authenticated: kwargs["headers"] = self.headers if request_type not in (GET, POST, PUT, DELETE): raise MyPermobilClientException("Invalid request type") try: if request_type == GET: return await self.session.get(*args, **kwargs) if request_type == POST: return await self.session.post(*args, **kwargs) if request_type == PUT: return await self.session.put(*args, **kwargs) if request_type == DELETE: return await self.session.delete(*args, **kwargs) except aiohttp.ClientConnectorError as err: raise MyPermobilConnectionException("Connection error") from err except asyncio.TimeoutError as err: raise MyPermobilConnectionException("Connection timeout") from err except aiohttp.ClientError as err: raise MyPermobilConnectionException("Client error") from err except Exception as err: raise MyPermobilAPIException("Unknown error") from err @cacheable async def request_regions( self, include_icons: bool = False, include_internal: bool = False ): """Get regions.""" if self.email and self.email.endswith("@permobil.com"): include_internal = True response = await self.make_request(GET, GET_REGIONS, headers={}) if response.status == 200: response_json = await response.json() regions = {} for region in response_json: if not include_internal: if ( region.get("backendPort") != 443 or region.get("serverType") != "Production" ): continue region_id = region.get("_id") regions[region_id] = {} regions[region_id]["name"] = region.get("name") regions[region_id]["port"] = region.get("backendPort") protocol = "https" if region.get("backendPort") == 443 else "http" regions[region_id]["url"] = f"{protocol}://{region.get('host')}" if include_icons: regions[region_id]["icon"] = region.get("flag") return regions if response.status in (404, 500): text = await response.text() raise MyPermobilAPIException(text) async def request_region_names(self, include_internal: bool = False): """Get region names.""" regions = await self.request_regions( include_icons=False, include_internal=include_internal ) return { regions[region_id].get("name"): regions[region_id].get("url") for region_id in regions } async def request_application_code( self, email: str = None, region: str = None, application: str = None ): """Post application link.""" if email is None: email = self.email if region is None: region = self.region if application is None: application = self.application if self.authenticated: raise MyPermobilClientException("Already authenticated") if not application: raise MyPermobilClientException("Missing application name") email = validate_email(email) region = validate_region(region) url = region + ENDPOINT_APPLICATIONLINKS json = {"username": email, "application": application} response = await self.make_request(POST, url, json=json) if response.status != 204: text = await response.text() raise MyPermobilAPIException(text) async def request_application_token( self, email: str = None, code: int = None, region: str = None, application: str = None, expiration_date: str = None, ) -> tuple: """Post the application token.""" if email is None: email = self.email if code is None: code = self.code if region is None: region = self.region if application is None: application = self.application if expiration_date is None: # set expiration date to 1 year from now time_delta = datetime.timedelta(days=365) date = datetime.datetime.now() + time_delta expiration_date = date.strftime("%Y-%m-%d") if self.authenticated: raise MyPermobilClientException("Already authenticated") email = validate_email(email) region = validate_region(region) code = validate_code(code) expiration_date = validate_expiration_date(expiration_date) url = region + ENDPOINT_APPLICATIONAUTHENTICATIONS json = { "username": email, "code": code, "application": application, "expirationDate": expiration_date, } response = await self.make_request(POST, url, json=json) if response.status == 200: json = await response.json() token = json.get("token") elif response.status == 401: raise MyPermobilAPIException("Email not registered for region") elif response.status == 403: raise MyPermobilAPIException("Incorrect code") elif response.status == 430: raise MyPermobilEulaException("Please accept the EULA") elif response.status in (400, 500): resp = await response.json() raise MyPermobilAPIException(resp.get("error", resp)) else: text = await response.text() raise MyPermobilAPIException(text) return token, expiration_date async def deauthenticate( self, email: str = None, region: str = None, application: str = None, headers: dict = None, ): """deauthenticate.""" if email is None: email = self.email if region is None: region = self.region if application is None: application = self.application if headers is None: headers = self.headers if not application: raise MyPermobilClientException("Missing application name") email = validate_email(email) region = validate_region(region) url = region + ENDPOINT_APPLICATIONLINKS json = {"application": application} response = await self.make_request(DELETE, url, json=json, headers=headers) if response.status != 204: text = await response.text() raise MyPermobilAPIException(text) async def request_product_id(self, headers: dict = None) -> str: """Get product id from the API.""" # this function is equivalent to request_item(PRODUCTS_ID, ENDPOINT_PRODUCTS) # but it has better error handling if headers is None: headers = self.headers response = await self.request_endpoint(ENDPOINT_PRODUCTS, headers) if not isinstance(response, list): raise MyPermobilAPIException("Invalid response") if len(response) != 1: raise MyPermobilAPIException("Wrong number of products found") return response[PRODUCTS_ID[0]][PRODUCTS_ID[1]] async def request_item( self, items: str | list[str], endpoint: str = None, **kwargs ) -> str | int | float | bool | dict | list: """Takes a single item or list of items, finds the endpoint and makes the request.""" if not items: raise MyPermobilClientException("No item(s) provided") if isinstance(items, str): items = [items] if endpoint is None: key = str(items) if key in ENDPOINT_LOOKUP: endpoint = ENDPOINT_LOOKUP.get(key) else: raise MyPermobilClientException(f"No endpoint for: {key}") # dive into the response for each item in the list response = await self.request_endpoint(endpoint, kwargs) for item in items: if isinstance(response, dict) and item not in response: raise MyPermobilClientException(f"{item} not in response") if isinstance(response, list) and item >= len(response): raise MyPermobilClientException( f"Too few items in response {item} >= {len(response)}" ) response = response[item] return response @cacheable async def request_endpoint( self, endpoint: str, headers: dict = None, product_id: str = None ) -> dict: """Makes a request to an endpoint.""" if headers is None: headers = self.headers if product_id is None: product_id = self.product_id endpoint = self.region + endpoint.format(product_id=product_id) resp = await self.make_request(GET, endpoint, headers=headers) status = resp.status try: json = await resp.json() if status >= 200 and status < 300: return json text = await resp.text() message = json.get("error", text) except aiohttp.client_exceptions.ContentTypeError: message = await resp.text() raise MyPermobilAPIException(f"{status}: {message}") async def get_battery_info(self) -> dict: """ request battery info """ return await self.request_endpoint(ENDPOINT_BATTERY_INFO) async def get_daily_usage(self) -> dict: """ request daily usage info """ return await self.request_endpoint(ENDPOINT_DAILY_USAGE) async def get_usage_records(self) -> dict: """ request records info """ return await self.request_endpoint(ENDPOINT_VA_USAGE_RECORDS) async def get_gps_position(self) -> dict: """ request gps info """ return await self.request_endpoint(ENDPOINT_PRODUCTS_POSITIONS) python-mypermobil-0.1.8/mypermobil/py.typed000066400000000000000000000000331470617340500210770ustar00rootroot00000000000000# Marker file for PEP 561. python-mypermobil-0.1.8/pyproject.toml000066400000000000000000000001251470617340500201370ustar00rootroot00000000000000[build-system] requires = ['setuptools>=42'] build-backend = 'setuptools.build_meta' python-mypermobil-0.1.8/requirements.txt000066400000000000000000000000201470617340500205010ustar00rootroot00000000000000aiohttp aiocachepython-mypermobil-0.1.8/setup.cfg000066400000000000000000000010721470617340500170460ustar00rootroot00000000000000[metadata] name = mypermobil version = 0.1.8 author = Isak Nyberg author_email = isak@nyberg.dev description = A python wrapper for a subset of the MyPermobil API long_description = file: README.md, LICENSE long_description_content_type = text/markdown url = https://github.com/IsakNyberg/MyPermobil-API project_urls = Bug Tracker = https://github.com/IsakNyberg/MyPermobil-API/issues repository = https://github.com/IsakNyberg/MyPermobil-API [options] package_dir = = . packages = ["mypermobil"] python_requires = >=3.6 [options.packages.find] where = . python-mypermobil-0.1.8/setup.py000066400000000000000000000007751470617340500167500ustar00rootroot00000000000000import setuptools setuptools.setup( name="mypermobil", version="0.1.8", description="A Python wrapper for the MyPermobil API", url="https://github.com/IsakNyberg/MyPermobil-API", author="Isak Nyberg", author_email="isak@nyberg.dev", license="MIT", packages=["mypermobil"], install_requires=["aiohttp", "aiocache"], test_requires=["pytest", "aiounittest", "aiocache"], long_description=open("README.md").read(), long_description_content_type="text/markdown", ) python-mypermobil-0.1.8/tests/000077500000000000000000000000001470617340500163675ustar00rootroot00000000000000python-mypermobil-0.1.8/tests/__init__.py000066400000000000000000000000001470617340500204660ustar00rootroot00000000000000python-mypermobil-0.1.8/tests/auth_test.py000066400000000000000000000142001470617340500207360ustar00rootroot00000000000000""" test auth control flow """ import aiounittest import datetime import unittest from unittest.mock import MagicMock, AsyncMock from mypermobil import MyPermobil, MyPermobilClientException, MyPermobilAPIException, MyPermobilEulaException # pylint: disable=missing-docstring class TestAuth(aiounittest.AsyncTestCase): def setUp(self): self.api = MyPermobil( "test", AsyncMock(), region="http://example.com", ) async def test_not_authenticated(self): with self.assertRaises(MyPermobilClientException): _ = self.api.headers assert not self.api.authenticated async def test_request_application_code(self): self.api.set_email("valid@email.com") self.api.set_region("http://example.com") resp = MagicMock(status=204) resp.text = AsyncMock(return_value="OK") self.api.make_request = AsyncMock(return_value=resp) await self.api.request_application_code() async def test_request_application_code_invalid_response(self): resp = MagicMock(status=400) resp.text = AsyncMock(return_value="Error") self.api.make_request = AsyncMock(return_value=resp) with self.assertRaises(MyPermobilAPIException): await self.api.request_application_code( email="test@example.com", region="http://example.com", application="myapp", ) async def test_request_application_code_client_errors(self): resp = MagicMock(status=400) resp.text = AsyncMock(return_value="Error") self.api.authenticated = True with self.assertRaises(MyPermobilClientException): await self.api.request_application_code( email="test@example.com", region="http://example.com", ) self.api.authenticated = False self.api.application = "" with self.assertRaises(MyPermobilClientException): await self.api.request_application_code( email="test@example.com", region="http://example.com", application="", ) async def test_request_application_token(self): resp = AsyncMock(status=200) resp.json = AsyncMock(return_value={"token": "mytoken"}) self.api.make_request = AsyncMock(return_value=resp) self.api.set_email("test@example.com") self.api.set_code("123123") result = await self.api.request_application_token() expected_date = datetime.datetime.now() + datetime.timedelta(days=365) self.assertEqual(result, ("mytoken", expected_date.strftime("%Y-%m-%d"))) # test that it cannot be done twice self.api.authenticated = True with self.assertRaises(MyPermobilClientException): await self.api.request_application_token() async def test_request_application_token_invalid_response(self): self.api.make_request = AsyncMock(return_value=MagicMock(status=401)) with self.assertRaises(MyPermobilAPIException): await self.api.request_application_token( email="test@example.com", code="123123" ) self.api.make_request = AsyncMock(return_value=MagicMock(status=403)) with self.assertRaises(MyPermobilAPIException): await self.api.request_application_token( email="test@example.com", code="123123" ) resp = AsyncMock(status=400) resp.json = AsyncMock(return_value={"error": "test"}) self.api.make_request = AsyncMock(return_value=resp) with self.assertRaises(MyPermobilAPIException): await self.api.request_application_token( email="test@example.com", code="123123" ) resp = AsyncMock(status=430) resp.json = AsyncMock(return_value={"error": "test"}) self.api.make_request = AsyncMock(return_value=resp) with self.assertRaises(MyPermobilEulaException): await self.api.request_application_token( email="test@example.com", code="123123" ) resp = AsyncMock(status=123123) resp.test = AsyncMock(return_value="test") self.api.make_request = AsyncMock(return_value=resp) with self.assertRaises(MyPermobilAPIException): await self.api.request_application_token( email="test@example.com", code="123123" ) async def test_reauthenticate(self): self.api.authenticated = True with self.assertRaises(MyPermobilClientException): self.api.self_reauthenticate() self.api.authenticated = False self.api.application = "" with self.assertRaises(MyPermobilClientException): self.api.self_reauthenticate() self.api.authenticated = False self.api.application = "test" self.api.self_reauthenticate() assert self.api.authenticated is False assert self.api.token == None assert self.api.expiration_date == None assert self.api.code == None async def test_deauthenticate(self): # test deauth when not authenticated self.api.authenticated = False self.api.set_email("test@example.com") self.api.set_code("123123") with self.assertRaises(MyPermobilClientException) as e: await self.api.deauthenticate() assert str(e.exception) == "Not authenticated" # test deauth when not application self.api.authenticated = True self.api.application = "" with self.assertRaises(MyPermobilClientException) as e: await self.api.deauthenticate() assert str(e.exception) == "Missing application name" self.api.authenticated = True self.api.application = "test" # test successful deauth text = AsyncMock(return_value="text") session = AsyncMock() session.make_request = AsyncMock(return_value=AsyncMock(status=123, text=text)) self.api.session = session with self.assertRaises(MyPermobilAPIException): await self.api.deauthenticate() if __name__ == "__main__": unittest.main() python-mypermobil-0.1.8/tests/item_test.py000066400000000000000000000251311470617340500207400ustar00rootroot00000000000000""" test auth control flow """ import aiounittest import datetime import unittest import aiohttp import asyncio from unittest.mock import AsyncMock from mypermobil import ( MyPermobil, MyPermobilClientException, MyPermobilAPIException, MyPermobilConnectionException, BATTERY_AMPERE_HOURS_LEFT, RECORDS_DISTANCE, RECORDS_SEATING, ENDPOINT_VA_USAGE_RECORDS, GET, POST, DELETE, PUT, ) # pylint: disable=missing-docstring class TestRequest(aiounittest.AsyncTestCase): def setUp(self): ttl = datetime.datetime.now() + datetime.timedelta(days=1) self.api = MyPermobil( "test", AsyncMock(), email="valid@email.com", region="http://example.com", code="123456", token="a" * 256, expiration_date=ttl.strftime("%Y-%m-%d"), ) self.api.self_authenticate() async def test_request_item(self): resp = AsyncMock(status=200) resp.json = AsyncMock(return_value={BATTERY_AMPERE_HOURS_LEFT[0]: 123}) self.api.make_request = AsyncMock(return_value=resp) res = await self.api.request_item(BATTERY_AMPERE_HOURS_LEFT) assert res == 123 assert self.api.make_request.call_count == 1 async def test_request_item_404(self): status = 404 msg = "not found" resp = AsyncMock(status=status) resp.json = AsyncMock(return_value={"error": msg}) self.api.make_request = AsyncMock(return_value=resp) with self.assertRaises(MyPermobilAPIException): await self.api.request_item(BATTERY_AMPERE_HOURS_LEFT) async def test_request_non_existent_item(self): item = "this item is not an item that exists" with self.assertRaises(MyPermobilClientException): await self.api.request_item(item) async def test_request_empty_item(self): item = [] with self.assertRaises(MyPermobilClientException): await self.api.request_item(item) async def test_request_invalid_item(self): # scenario one, a list that does not have enough items items = [100] # mock request endpoint result resp = [1, 2, 3] self.api.request_endpoint = AsyncMock(return_value=resp) with self.assertRaises(MyPermobilClientException): await self.api.request_item(items, endpoint="test") # scenario two, dict that does not have specific item item = "i want this item" items = [item] # mock request endpoint result resp = {"i only have this item": 123} self.api.request_endpoint = AsyncMock(return_value=resp) with self.assertRaises(MyPermobilClientException): await self.api.request_item(items, endpoint="test") async def test_request_endpoint_ContentTypeError(self): # scenario one, a list that does not have enough items resp = AsyncMock(status=400) mock = AsyncMock() resp.json = AsyncMock( side_effect=aiohttp.client_exceptions.ContentTypeError(mock, mock) ) self.api.make_request = AsyncMock(return_value=resp) with self.assertRaises(MyPermobilAPIException): await self.api.request_endpoint("endpoint") # async def test_request_non_existent_endpoint(self): # endpoint = "this endpoint does not exist" # item = "invalid item" # with self.assertRaises(MyPermobilClientException): # await self.api.request_item(item, endpoint=endpoint) # async def test_request_non_invalid_endpoint(self): # endpoint = "this endpoint does not exist" # item = RECORDS_SEATING # with self.assertRaises(MyPermobilClientException): # await self.api.request_item(item, endpoint=endpoint) async def test_request_request_endpoint_cache(self): """call the same endpoint twice and check that the cache is used""" resp = AsyncMock(status=200) resp.json = AsyncMock( return_value={RECORDS_DISTANCE[0]: 123, RECORDS_SEATING[0]: 456} ) self.api.make_request = AsyncMock(return_value=resp) res1 = await self.api.request_item(RECORDS_DISTANCE) res2 = await self.api.request_item(RECORDS_SEATING) assert res1 == 123 assert res2 == 456 assert self.api.make_request.call_count == 1 assert self.api.make_request.call_args[0][1].endswith(ENDPOINT_VA_USAGE_RECORDS) async def test_request_request_endpoint_async(self): """call the same endpoint twice and check that the cache is used""" async def delay(): await asyncio.sleep(0.5) return {RECORDS_DISTANCE[0]: 123, RECORDS_SEATING[0]: 456} resp = AsyncMock(status=200) resp.json = AsyncMock(side_effect=delay) self.api.make_request = AsyncMock(return_value=resp) task1 = asyncio.create_task(self.api.request_item(RECORDS_DISTANCE)) task2 = asyncio.create_task(self.api.request_item(RECORDS_SEATING)) res1 = await task1 res2 = await task2 assert res1 == 123 assert res2 == 456 assert self.api.make_request.call_count == 1 assert self.api.make_request.call_args[0][1].endswith(ENDPOINT_VA_USAGE_RECORDS) async def test_request_request_endpoint_cache_exception(self): status = 404 msg = "not found" resp = AsyncMock(status=status) resp.json = AsyncMock(return_value={"error": msg}) self.api.make_request = AsyncMock(return_value=resp) with self.assertRaises(MyPermobilAPIException): await self.api.request_item(RECORDS_DISTANCE) with self.assertRaises(MyPermobilAPIException): await self.api.request_item(RECORDS_SEATING) assert self.api.make_request.call_count == 1 assert self.api.make_request.call_args[0][1].endswith(ENDPOINT_VA_USAGE_RECORDS) async def test_request_get_request(self): session = AsyncMock(status=200) session.get = AsyncMock(return_value=AsyncMock(status=200)) self.api.session = session res = await self.api.make_request(GET, "http://example.com") assert res.status == 200 async def test_request_get_request_exceptions(self): session = AsyncMock() mock = AsyncMock() session.get = AsyncMock(side_effect=aiohttp.ClientConnectorError(mock, mock)) self.api.session = session with self.assertRaises(MyPermobilConnectionException): await self.api.make_request(GET, "http://example.com") session = AsyncMock() session.get = AsyncMock(side_effect=asyncio.TimeoutError()) self.api.session = session with self.assertRaises(MyPermobilConnectionException): await self.api.make_request(GET, "http://example.com") session = AsyncMock() session.get = AsyncMock(side_effect=aiohttp.ClientError(None, None)) self.api.session = session with self.assertRaises(MyPermobilConnectionException): await self.api.make_request(GET, "http://example.com") session = AsyncMock() session.get = AsyncMock(side_effect=ValueError()) self.api.session = session with self.assertRaises(MyPermobilAPIException): await self.api.make_request(GET, "http://example.com") async def test_request_post_request_exceptions(self): session = AsyncMock() mock = AsyncMock() session.post = AsyncMock(side_effect=aiohttp.ClientConnectorError(mock, mock)) self.api.session = session with self.assertRaises(MyPermobilConnectionException): await self.api.make_request(POST, "http://example.com") session = AsyncMock() session.post = AsyncMock(side_effect=asyncio.TimeoutError()) self.api.session = session with self.assertRaises(MyPermobilConnectionException): await self.api.make_request(POST, "http://example.com") session = AsyncMock() session.post = AsyncMock(side_effect=aiohttp.ClientError(None, None)) self.api.session = session with self.assertRaises(MyPermobilConnectionException): await self.api.make_request(POST, "http://example.com") session = AsyncMock() session.post = AsyncMock(side_effect=ValueError()) self.api.session = session with self.assertRaises(MyPermobilAPIException): await self.api.make_request(POST, "http://example.com") async def test_request_product_id(self): ttl = datetime.datetime.now() + datetime.timedelta(days=1) self.api = MyPermobil( "test", AsyncMock(), email="valid@email.com", region="http://example.com", code="123456", token="a" * 256, expiration_date=ttl.strftime("%Y-%m-%d"), ) self.api.self_authenticate() response = [{"_id": "123"}] request_endpoint_mock = AsyncMock(return_value=response) self.api.request_endpoint = request_endpoint_mock await self.api.request_product_id() # test for the places it can fail # list is not length 1 response = [{"_id": "123"}, {"_id": "123"}, {"_id": "123"}] request_endpoint_mock = AsyncMock(return_value=response) self.api.request_endpoint = request_endpoint_mock with self.assertRaises(MyPermobilAPIException): await self.api.request_product_id() # response it not a list response = "not a list" request_endpoint_mock = AsyncMock(return_value=response) self.api.request_endpoint = request_endpoint_mock with self.assertRaises(MyPermobilAPIException): await self.api.request_product_id() async def test_request_invalid_request_type(self): with self.assertRaises(MyPermobilClientException): await self.api.make_request("invalid request type", "http://example.com") async def test_request_put(self): # there is no put request in the api so this is the test session = AsyncMock() session.put = AsyncMock(return_value=AsyncMock(status=200)) self.api.session = session resp = await self.api.make_request(PUT, "http://example.com") assert resp.status == 200 async def test_request_delete(self): # there is no put request in the api so this is the test session = AsyncMock() session.delete = AsyncMock(return_value=AsyncMock(status=200)) self.api.session = session resp = await self.api.make_request(DELETE, "http://example.com") assert resp.status == 200 if __name__ == "__main__": unittest.main() python-mypermobil-0.1.8/tests/misc_test.py000066400000000000000000000014201470617340500207300ustar00rootroot00000000000000""" test auth control flow """ import aiounittest import datetime import unittest from unittest.mock import MagicMock, AsyncMock from mypermobil import MyPermobil, MyPermobilClientException, MyPermobilAPIException # pylint: disable=missing-docstring class TestAuth(aiounittest.AsyncTestCase): def setUp(self): self.api = MyPermobil( "test", AsyncMock(), region="http://example.com", ) def test_str(self): x = str(self.api) assert x async def test_close_session(self): await self.api.close_session() assert self.api.session is None with self.assertRaises(MyPermobilClientException): await self.api.close_session() if __name__ == "__main__": unittest.main() python-mypermobil-0.1.8/tests/region_test.py000066400000000000000000000031751470617340500212710ustar00rootroot00000000000000"""Test the region names request""" import asyncio import unittest from unittest.mock import AsyncMock from mypermobil import MyPermobil, create_session, MyPermobilAPIException class TestRegion(unittest.TestCase): """Test the region names request""" def test_region(self): """Test the region names request""" async def region_name_test(): """Test the region names request""" session = await create_session() api = MyPermobil("test", session) api.set_email("email.that.ends.with@permobil.com") names = await api.request_region_names() await api.close_session() assert len(names) > 0 async def region_with_flags(): """Test the region with flags request""" session = await create_session() api = MyPermobil("test", session) regions = await api.request_regions(include_icons=True) await api.close_session() assert len(regions) > 0 async def region_error(): """Test the region error request""" session = AsyncMock() response = AsyncMock() response.status = 404 response.text = AsyncMock(return_value="test") session.get = AsyncMock(return_value=response) api = MyPermobil("test", session) with self.assertRaises(MyPermobilAPIException): await api.request_regions(include_icons=True) asyncio.run(region_name_test()) asyncio.run(region_with_flags()) asyncio.run(region_error()) if __name__ == "__main__": unittest.main() python-mypermobil-0.1.8/tests/requests_test.py000066400000000000000000000027231470617340500216570ustar00rootroot00000000000000""" test auth control flow """ import aiounittest import datetime import unittest import aiohttp import asyncio from unittest.mock import AsyncMock from mypermobil import MyPermobil # pylint: disable=missing-docstring class TestRequest(aiounittest.AsyncTestCase): def setUp(self): ttl = datetime.datetime.now() + datetime.timedelta(days=1) self.api = MyPermobil( "test", AsyncMock(), email="valid@email.com", region="http://example.com", code="123456", token="a" * 256, expiration_date=ttl.strftime("%Y-%m-%d"), ) self.api.self_authenticate() async def test_battery_info(self) -> dict: """ test battery info """ self.api.request_endpoint = AsyncMock(return_value={}) return await self.api.get_battery_info() async def test_daily_usage(self) -> dict: """ test daily usage info """ self.api.request_endpoint = AsyncMock(return_value={}) return await self.api.get_daily_usage() async def test_records_info(self) -> dict: """ test records info """ self.api.request_endpoint = AsyncMock(return_value={}) return await self.api.get_usage_records() async def test_gps_info(self) -> dict: """ test gps info """ self.api.request_endpoint = AsyncMock(return_value={}) return await self.api.get_gps_position() if __name__ == "__main__": unittest.main() python-mypermobil-0.1.8/tests/validation_test.py000066400000000000000000000127541470617340500221430ustar00rootroot00000000000000""" test auth control flow """ import aiounittest import datetime import unittest from unittest.mock import MagicMock, AsyncMock from mypermobil import MyPermobil, MyPermobilClientException, MyPermobilAPIException # pylint: disable=missing-docstring class TestAuth(aiounittest.AsyncTestCase): def setUp(self): self.api = MyPermobil( "", AsyncMock(), ) async def test_set_email(self): email = "valid@email.com" self.api.set_email(email) self.assertEqual(self.api.email, email) email = "invalid" with self.assertRaises(MyPermobilClientException): self.api.set_email(email) email = "" with self.assertRaises(MyPermobilClientException): self.api.set_email(email) async def test_set_expiration_date(self): date = datetime.datetime.now() + datetime.timedelta(days=1) prev_date = datetime.datetime.now() - datetime.timedelta(days=1) expiration_date = date.strftime("%Y-%m-%d") self.api.set_expiration_date(expiration_date) self.assertEqual(self.api.expiration_date, expiration_date) expiration_date = "invalid" with self.assertRaises(MyPermobilClientException): self.api.set_expiration_date(expiration_date) expiration_date = prev_date.strftime("%Y-%m-%d") with self.assertRaises(MyPermobilClientException): self.api.set_expiration_date(expiration_date) expiration_date = "" with self.assertRaises(MyPermobilClientException): self.api.set_expiration_date(expiration_date) async def test_set_token(self): token = "a" * 256 self.api.set_token(token) self.assertEqual(self.api.token, token) token = "invalid" with self.assertRaises(MyPermobilClientException): self.api.set_token(token) token = "" with self.assertRaises(MyPermobilClientException): self.api.set_token(token) async def test_set_code(self): code = "123456" self.api.set_code(code) self.assertEqual(self.api.code, code) code = "invalid" with self.assertRaises(MyPermobilClientException): self.api.set_code(code) code = "123456789" with self.assertRaises(MyPermobilClientException): self.api.set_code(code) code = "" with self.assertRaises(MyPermobilClientException): self.api.set_code(code) code = " " with self.assertRaises(MyPermobilClientException): self.api.set_code(code) async def test_set_region(self): region = "http://example.com" self.api.set_region(region) self.assertEqual(self.api.region, region) region = "not a valid region" with self.assertRaises(MyPermobilClientException): self.api.set_region(region) region = "" with self.assertRaises(MyPermobilClientException): self.api.set_region(region) async def test_set_product_id(self): id = "a" * 24 self.api.set_product_id(id) self.assertEqual(self.api.product_id, id) region = "a" * 10 with self.assertRaises(MyPermobilClientException): self.api.set_product_id(region) region = "" with self.assertRaises(MyPermobilClientException): self.api.set_product_id(region) async def test_auth_checks(self): # cannot authenticate without setting application name with self.assertRaises(MyPermobilClientException): self.api.self_authenticate() # cannot deauth without authenticating with self.assertRaises(MyPermobilClientException): await self.api.deauthenticate() self.api.set_email("valid@email.com") self.api.set_region("http://example.com") self.api.set_code("123456") self.api.set_token("a" * 256) date = datetime.datetime.now() + datetime.timedelta(days=1) self.api.set_expiration_date(date.strftime("%Y-%m-%d")) self.api.set_application("test") self.api.self_authenticate() # cannot authenticate twice with self.assertRaises(MyPermobilClientException): self.api.self_authenticate() async def test_change_after_auth(self): self.api.set_email("valid@email.com") self.api.set_region("http://example.com") self.api.set_code("123456") self.api.set_token("a" * 256) date = datetime.datetime.now() + datetime.timedelta(days=1) self.api.set_expiration_date(date.strftime("%Y-%m-%d")) self.api.set_application("test") self.api.self_authenticate() assert self.api.headers == {"Authorization": f"Bearer {'a' * 256}"} with self.assertRaises(MyPermobilClientException): self.api.set_email("valid2@email.com") with self.assertRaises(MyPermobilClientException): self.api.set_region("http://new_region.com") with self.assertRaises(MyPermobilClientException): self.api.set_code("654321") with self.assertRaises(MyPermobilClientException): self.api.set_token("b" * 256) date = datetime.datetime.now() - datetime.timedelta(days=2) with self.assertRaises(MyPermobilClientException): self.api.set_expiration_date(date.strftime("%Y-%m-%d")) with self.assertRaises(MyPermobilClientException): self.api.set_application("new_application") if __name__ == "__main__": unittest.main()