From 19acd90889bd35a9ca2fbdda6404d2d98151844b Mon Sep 17 00:00:00 2001 From: Jan Date: Mon, 6 Nov 2023 13:04:06 +0100 Subject: [PATCH] Add FIO storage endpoints --- docs/endpoints/storage.md | 14 ++ .../endpoints/abstracts/abstract_sites.py | 3 + .../endpoints/abstracts/abstract_storage.py | 9 + fio_wrapper/endpoints/endpoints_v1/sites.py | 15 ++ fio_wrapper/endpoints/endpoints_v1/storage.py | 100 +++++++++ fio_wrapper/exceptions.py | 9 + fio_wrapper/fio.py | 4 + fio_wrapper/models/storage_models.py | 39 ++++ fio_wrapper/urls.py | 17 ++ mkdocs.yml | 7 +- tests/test_storage_v1.py | 199 ++++++++++++++++++ tests/test_urls.py | 22 ++ 12 files changed, 435 insertions(+), 3 deletions(-) create mode 100644 docs/endpoints/storage.md create mode 100644 fio_wrapper/endpoints/abstracts/abstract_storage.py create mode 100644 fio_wrapper/endpoints/endpoints_v1/storage.py create mode 100644 fio_wrapper/models/storage_models.py create mode 100644 tests/test_storage_v1.py diff --git a/docs/endpoints/storage.md b/docs/endpoints/storage.md new file mode 100644 index 0000000..05942b8 --- /dev/null +++ b/docs/endpoints/storage.md @@ -0,0 +1,14 @@ +Exposes FIO Storage data for users. +All storage data requires a **FIO API KEY**. You will need to have access to the data the user has in FIO in order to access it. + +Example: +```python +from fio_wrapper import FIO + +fio = FIO(api_key="your_api_key") + +# Get users storage data +user_storage = fio.Sites.get(username="PrUn username") +``` + +::: endpoints.endpoints_v1.storage \ No newline at end of file diff --git a/fio_wrapper/endpoints/abstracts/abstract_sites.py b/fio_wrapper/endpoints/abstracts/abstract_sites.py index 0e5f7ad..4c3f192 100644 --- a/fio_wrapper/endpoints/abstracts/abstract_sites.py +++ b/fio_wrapper/endpoints/abstracts/abstract_sites.py @@ -2,5 +2,8 @@ class AbstractSites: def get(self, username: str): raise NotImplemented() + def get_planet(self, username: str, planet: str): + raise NotImplemented() + def planets(self, username: str): raise NotImplemented() diff --git a/fio_wrapper/endpoints/abstracts/abstract_storage.py b/fio_wrapper/endpoints/abstracts/abstract_storage.py new file mode 100644 index 0000000..ee62dc4 --- /dev/null +++ b/fio_wrapper/endpoints/abstracts/abstract_storage.py @@ -0,0 +1,9 @@ +class AbstractStorage: + def get(self, username: str): + raise NotImplemented() + + def get_specific(self, username: str, specific: str): + raise NotImplemented() + + def planets(self, username: str): + raise NotImplemented() diff --git a/fio_wrapper/endpoints/endpoints_v1/sites.py b/fio_wrapper/endpoints/endpoints_v1/sites.py index 6a3583b..6d3235a 100644 --- a/fio_wrapper/endpoints/endpoints_v1/sites.py +++ b/fio_wrapper/endpoints/endpoints_v1/sites.py @@ -105,6 +105,21 @@ def planets(self, username: str) -> List[str]: @apikey_required def warehouses(self, username: str) -> WarehouseList: + """Get warehouse data for username from FIO + + Note: + FIO API Key Required + + Args: + username (str): Prosperous Universe username + + Raises: + NoSiteData: Username has no warehouse site data + NotAuthenticated: Not authenticated or no appropiate permissions + + Returns: + WarehouseList: List of Warehouses + """ (status, data) = self._adapter.get( endpoint=self._adapter.urls.sites_warehouses_get(username=username), err_codes=[204, 401], diff --git a/fio_wrapper/endpoints/endpoints_v1/storage.py b/fio_wrapper/endpoints/endpoints_v1/storage.py new file mode 100644 index 0000000..395e368 --- /dev/null +++ b/fio_wrapper/endpoints/endpoints_v1/storage.py @@ -0,0 +1,100 @@ +from typing import List +from fio_wrapper.decorator import apikey_required +from fio_wrapper.endpoints.abstracts.abstract_endpoint import AbstractEndpoint +from fio_wrapper.endpoints.abstracts.abstract_storage import AbstractStorage +from fio_wrapper.exceptions import NoStorageData, NotAuthenticated +from fio_wrapper.models.storage_models import StorageList, Storage as StorageModel + + +class Storage(AbstractStorage, AbstractEndpoint): + @apikey_required + def get(self, username: str) -> StorageList: + """Gets users storage data from FIO + + Note: + FIO API Key Required + + Args: + username (str): Prosperous Universe username + + Raises: + NoStorageData: Username has no storage data + NotAuthenticated: Not authenticated or no appropiate permissions + + Returns: + StorageList: List of storages + """ + (status, data) = self._adapter.get( + endpoint=self._adapter.urls.storage_get_url(username=username), + err_codes=[204, 401], + ) + + if status == 200: + return StorageList.model_validate(data) + + elif status == 204: + raise NoStorageData("Username has no storage data") + elif status == 401: + raise NotAuthenticated("Not authenticated or no appropiate permissions") + + @apikey_required + def get_specific(self, username: str, specific: str) -> StorageModel: + """Gets users specific storage data from FIO + + Note: + FIO API Key Required + + Args: + username (str): Prosperous Universe username + specific (str): StorageId, PlanetId, PlanetNaturalId or PlanetName + + Raises: + NoStorageData: Username has no storage data + NotAuthenticated: Not authenticated or no appropiate permissions + + Returns: + StorageModel: Storage data + """ + (status, data) = self._adapter.get( + endpoint=self._adapter.urls.storage_get_specific_url( + username=username, specific=specific + ), + err_codes=[204, 401], + ) + + if status == 200: + return StorageModel.model_validate(data) + + elif status == 204: + raise NoStorageData("Username has no storage data") + elif status == 401: + raise NotAuthenticated("Not authenticated or no appropiate permissions") + + @apikey_required + def planets(self, username: str) -> List[str]: + """Returns a list of storages from FIO + + Note: + FIO API Key Required + + Args: + username (str): Prosperous Universe username + + Raises: + NoStorageData: Username has no storage data + NotAuthenticated: Not authenticated or no appropiate permissions + + Returns: + List[str]: List of StorageIds + """ + (status, data) = self._adapter.get( + endpoint=self._adapter.urls.storage_planets_get_url(username=username), + err_codes=[204, 401], + ) + + if status == 200: + return data + elif status == 204: + raise NoStorageData("Username has no storage data") + elif status == 401: + raise NotAuthenticated("Not authenticated or no appropiate permissions") diff --git a/fio_wrapper/exceptions.py b/fio_wrapper/exceptions.py index 56f1148..895788a 100644 --- a/fio_wrapper/exceptions.py +++ b/fio_wrapper/exceptions.py @@ -125,3 +125,12 @@ class NoSiteData(Exception): """No site data found""" pass + + +# Storage + + +class NoStorageData(Exception): + """No storage data found""" + + pass diff --git a/fio_wrapper/fio.py b/fio_wrapper/fio.py index 28e5bda..125500f 100644 --- a/fio_wrapper/fio.py +++ b/fio_wrapper/fio.py @@ -11,6 +11,7 @@ from fio_wrapper.endpoints.endpoints_v1 import planet as planet_v1 from fio_wrapper.endpoints.endpoints_v1 import recipe as recipe_v1 from fio_wrapper.endpoints.endpoints_v1 import sites as sites_v1 +from fio_wrapper.endpoints.endpoints_v1 import storage as storage_v1 class FIO: @@ -23,6 +24,8 @@ class FIO: Material (Material): Material information Planet (Planet): Planet information Recipe (Recipe): Recipe information + Sites (Sites): Sites information + Storage (Storage): Storage information """ def __init__( @@ -53,6 +56,7 @@ def __init__( self.Planet = planet_v1.Planet(self._adapter) self.Recipe = recipe_v1.Recipe(self._adapter) self.Sites = sites_v1.Sites(self._adapter) + self.Storage = storage_v1.Storage(self._adapter) else: raise EndpointNotImplemented() diff --git a/fio_wrapper/models/storage_models.py b/fio_wrapper/models/storage_models.py new file mode 100644 index 0000000..c1e3b01 --- /dev/null +++ b/fio_wrapper/models/storage_models.py @@ -0,0 +1,39 @@ +from typing import List, Optional +from datetime import datetime +from pydantic import BaseModel, RootModel, Field, NaiveDatetime + + +class StorageItem(BaseModel): + MaterialId: str = Field(min_length=32) + MaterialName: Optional[str] + MaterialTicker: Optional[str] = Field(max_length=3, default=None) + MaterialCategory: Optional[str] = Field(min_length=32, default=None) + MaterialWeight: float + MaterialVolume: float + MaterialAmount: int + MaterialValue: float + MaterialValueCurrency: Optional[str] + Type: str + TotalWeight: float + TotalVolume: float + + +class Storage(BaseModel): + StorageItems: Optional[List[StorageItem]] + StorageId: str = Field(min_length=32) + AddressableId: str = Field(min_length=32) + Name: Optional[str] + Type: str + UserNameSubmitted: str + Timestamp: datetime + WeightCapacity: int + VolumeCapacity: int + UserNameSubmitted: str + Timestamp: NaiveDatetime + + +class StorageList(RootModel): + root: List[Storage] + + def __iter__(self): + return iter(self.root) diff --git a/fio_wrapper/urls.py b/fio_wrapper/urls.py index 6d63a0a..8fd572d 100644 --- a/fio_wrapper/urls.py +++ b/fio_wrapper/urls.py @@ -39,6 +39,10 @@ def __init__(self, base_url: str) -> None: self.sites_planets = "/planets" self.sites_warehouses = "/warehouses" + # storage + self.storage_base = "/storage" + self.storage_planets = "/planets" + # Material def material_url(self) -> str: return self.base_url + self.material_base @@ -168,3 +172,16 @@ def sites_planets_get_planet_url(self, username: str, planet: str) -> str: def sites_warehouses_get(self, username: str) -> str: return self.sites_url() + self.sites_warehouses + "/" + username + + # Storage + def storage_url(self) -> str: + return self.base_url + self.storage_base + + def storage_get_url(self, username: str) -> str: + return self.storage_url() + "/" + username + + def storage_planets_get_url(self, username: str) -> str: + return self.storage_url() + self.storage_planets + "/" + username + + def storage_get_specific_url(self, username: str, specific: str) -> str: + return self.storage_url() + "/" + username + "/" + specific diff --git a/mkdocs.yml b/mkdocs.yml index 37491b2..fb228e3 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -84,12 +84,13 @@ nav: - Decorators: decorator.md - Exceptions: exception.md - Endpoints (FIO v.1): - - Planet: 'endpoints/planet.md' - Building: 'endpoints/building.md' - - Recipe: 'endpoints/recipe.md' - - Material: 'endpoints/material.md' - Exchange: 'endpoints/exchange.md' - LocalMarket: 'endpoints/localmarket.md' + - Material: 'endpoints/material.md' + - Planet: 'endpoints/planet.md' + - Recipe: 'endpoints/recipe.md' - Sites: 'endpoints/sites.md' + - Storage: 'endpoints/storage.md' - Test Coverage: - Coverage Report: coverage.md \ No newline at end of file diff --git a/tests/test_storage_v1.py b/tests/test_storage_v1.py new file mode 100644 index 0000000..2adae04 --- /dev/null +++ b/tests/test_storage_v1.py @@ -0,0 +1,199 @@ +import pytest +from typing import Dict +from fio_wrapper.exceptions import NoStorageData, NotAuthenticated +from fio_wrapper.fio import FIO +from fio_wrapper.models import storage_models + +from fio_wrapper.models.storage_models import Storage as StorageModel, StorageList +from .fixtures import ftx_fio_key + +storage_item_data = { + "MaterialId": "4fca6f5b5e6c3b8a1b887c6dc99db146", + "MaterialName": "drinkingWater", + "MaterialTicker": "DW", + "MaterialCategory": "3f047ec3043bdd795fd7272d6be98799", + "MaterialWeight": 0.10000000149011612, + "MaterialVolume": 0.10000000149011612, + "MaterialAmount": 1431, + "MaterialValue": 26633.2, + "MaterialValueCurrency": "NCC", + "Type": "INVENTORY", + "TotalWeight": 143.10000610351562, + "TotalVolume": 143.10000610351562, +} + +store_data = { + "StorageItems": [storage_item_data, storage_item_data], + "StorageId": "00e14a526b92f0c7675732f90b0a2aeb", + "AddressableId": "6eab22c0630c8081c4be75986dcb0780", + "Name": None, + "WeightLoad": 4018.305908203125, + "WeightCapacity": 6500.0, + "VolumeLoad": 2312.14990234375, + "VolumeCapacity": 6500.0, + "FixedStore": False, + "Type": "STORE", + "UserNameSubmitted": "SFSCORPIO", + "Timestamp": "2023-11-06T08:11:11.524793", +} + + +@pytest.fixture +def storage_item() -> Dict: + return storage_item_data + + +@pytest.fixture +def store() -> Dict: + return store_data + + +# Models + + +def test_model_StorageList(store) -> None: + data = StorageList.model_validate([store, store]) + for store in data: + assert type(store) is StorageModel + + +# Endpoints + + +@pytest.mark.parametrize( + "username, mock_status, json_data, return_data", + [ + ( + "foo", + 200, + [store_data, store_data], + StorageList.model_validate([store_data, store_data]), + ), + ( + "foo", + 204, + None, + NoStorageData, + ), + ( + "foo", + 401, + None, + NotAuthenticated, + ), + ], +) +def test_storage_get( + requests_mock, ftx_fio_key: FIO, username, mock_status, json_data, return_data +) -> None: + requests_mock.get( + ftx_fio_key._adapter.urls.storage_get_url(username=username), + status_code=mock_status, + json=json_data, + ) + + if return_data not in [NoStorageData, NotAuthenticated]: + data = ftx_fio_key.Storage.get(username=username) + + assert data == return_data + else: + with pytest.raises(return_data): + ftx_fio_key.Storage.get(username=username) + + +@pytest.mark.parametrize( + "username, specific, mock_status, json_data, return_data", + [ + ( + "foo", + "moo", + 200, + store_data, + StorageModel.model_validate(store_data), + ), + ( + "foo", + "moo", + 204, + None, + NoStorageData, + ), + ( + "foo", + "moo", + 401, + None, + NotAuthenticated, + ), + ], +) +def test_storage_get_specific( + requests_mock, + ftx_fio_key: FIO, + username, + specific, + mock_status, + json_data, + return_data, +) -> None: + requests_mock.get( + ftx_fio_key._adapter.urls.storage_get_specific_url( + username=username, specific=specific + ), + status_code=mock_status, + json=json_data, + ) + + if return_data not in [NoStorageData, NotAuthenticated]: + data = ftx_fio_key.Storage.get_specific(username=username, specific=specific) + + assert data == return_data + else: + with pytest.raises(return_data): + ftx_fio_key.Storage.get_specific(username=username, specific=specific) + + +@pytest.mark.parametrize( + "username, mock_status, json_data, return_data", + [ + ( + "foo", + 200, + ["abc", "def"], + ["abc", "def"], + ), + ( + "foo", + 204, + None, + NoStorageData, + ), + ( + "foo", + 401, + None, + NotAuthenticated, + ), + ], +) +def test_storage_planets( + requests_mock, + ftx_fio_key: FIO, + username, + mock_status, + json_data, + return_data, +) -> None: + requests_mock.get( + ftx_fio_key._adapter.urls.storage_planets_get_url(username=username), + status_code=mock_status, + json=json_data, + ) + + if return_data not in [NoStorageData, NotAuthenticated]: + data = ftx_fio_key.Storage.planets(username=username) + + assert data == return_data + else: + with pytest.raises(return_data): + ftx_fio_key.Storage.planets(username=username) diff --git a/tests/test_urls.py b/tests/test_urls.py index b76db8d..e10bbbb 100644 --- a/tests/test_urls.py +++ b/tests/test_urls.py @@ -157,3 +157,25 @@ def test_sites_planets_get_planet_url(url: URLs) -> None: def test_sites_warehouses_get(url: URLs) -> None: assert url.sites_warehouses_get(username="moo") == "foo/sites/warehouses/moo" + + +# Storage + + +def test_storage_url(url: URLs) -> None: + assert url.storage_url() == "foo/storage" + + +def test_storage_get_url(url: URLs) -> None: + assert url.storage_get_url(username="moo") == "foo/storage/moo" + + +def test_storage_planets_get_url(url: URLs) -> None: + assert url.storage_planets_get_url(username="moo") == "foo/storage/planets/moo" + + +def test_storage_get_specific_url(url: URLs) -> None: + assert ( + url.storage_get_specific_url(username="moo", specific="abc") + == "foo/storage/moo/abc" + )