pax_global_header00006660000000000000000000000064141464172700014520gustar00rootroot0000000000000052 comment=4ae7e62622e96618ff4d8f35581ea209c17bf77b nickw444-nsw-fuel-api-client-a0f90e7/000077500000000000000000000000001414641727000172655ustar00rootroot00000000000000nickw444-nsw-fuel-api-client-a0f90e7/.flake8000077500000000000000000000001411414641727000204370ustar00rootroot00000000000000[flake8] ignore = E261 exclude = .git, venv, virtualenv, docs, max-line-length = 90 nickw444-nsw-fuel-api-client-a0f90e7/.github/000077500000000000000000000000001414641727000206255ustar00rootroot00000000000000nickw444-nsw-fuel-api-client-a0f90e7/.github/workflows/000077500000000000000000000000001414641727000226625ustar00rootroot00000000000000nickw444-nsw-fuel-api-client-a0f90e7/.github/workflows/workflow.yaml000066400000000000000000000043201414641727000254170ustar00rootroot00000000000000on: push: schedule: - cron: '0 3 * * 6' jobs: check: name: "Check" runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - uses: actions/setup-python@v2 with: python-version: "3.x" - name: 'Install Dependencies' run: | python -m pip install --upgrade pip python -m pip install pipenv pipenv install --system --dev python -m pip install . - name: 'Check Formatting (flake8)' run: flake8 nsw_fuel nsw_fuel_tests - name: 'Check Types' run: mypy --strict nsw_fuel build: name: "Build and Test" runs-on: ubuntu-latest strategy: matrix: python-version: [3.8, 3.9, 3.x] steps: - uses: actions/checkout@v2 - uses: actions/setup-python@v2 with: python-version: ${{ matrix.python-version }} - name: 'Install Dependencies' run: | python -m pip install --upgrade pip python -m pip install pipenv wheel pipenv install --system --dev python -m pip install . - name: 'Run Tests (with coverage)' run: | coverage run --source=nsw_fuel setup.py test - uses: codecov/codecov-action@v2 if: ${{ matrix.python-version == '3.x' }} - name: "Build" run: | sed -i "s/0.0.0-dev/$(git describe --tags --exact-match)/" nsw_fuel/__init__.py python setup.py sdist bdist_wheel - uses: actions/upload-artifact@v2 # Only publish artifacts from Python latest build. if: ${{ matrix.python-version == '3.x' }} with: name: dist path: dist/ if-no-files-found: error release: name: "Release 🚀" runs-on: ubuntu-latest needs: - build - check if: startsWith(github.ref, 'refs/tags') steps: - uses: actions/download-artifact@v2 with: name: dist path: dist/ - name: Release to PyPi 📦 uses: pypa/gh-action-pypi-publish@master with: password: ${{ secrets.PYPI_API_TOKEN }} - name: Create Github Release uses: softprops/action-gh-release@v1 with: generate_release_notes: true nickw444-nsw-fuel-api-client-a0f90e7/.gitignore000077500000000000000000000013271414641727000212630ustar00rootroot00000000000000# Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] # C extensions *.so # Distribution / packaging .Python env/ build/ develop-eggs/ dist/ downloads/ eggs/ lib/ lib64/ parts/ sdist/ var/ *.egg-info/ .installed.cfg *.egg # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .coverage .cache nosetests.xml coverage.xml # Translations *.mo *.pot # Django stuff: *.log # Sphinx documentation docs/_build/ # PyBuilder target/ venv/ VERSION *.DS_Store .idea .mypy_cache .eggs nickw444-nsw-fuel-api-client-a0f90e7/.travis.yml000077500000000000000000000010011414641727000213710ustar00rootroot00000000000000language: python python: - "3.9" - "3.6" - "3.5" install: - pip install pipenv - pipenv install --system --dev - pip install coveralls flake8 mypy - pip install . script: - flake8 nsw_fuel nsw_fuel_tests - mypy --strict nsw_fuel - coverage run --source=nsw_fuel setup.py test after_success: coveralls before_deploy: - git describe --tags > VERSION deploy: provider: pypi skip_cleanup: true user: nickw444-deploy password: "$PYPI_PASSWORD" on: tags: true python: 3.6 nickw444-nsw-fuel-api-client-a0f90e7/LICENSE000077500000000000000000000020661414641727000203010ustar00rootroot00000000000000The MIT License (MIT) Copyright (c) 2018 Nick Whyte 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. nickw444-nsw-fuel-api-client-a0f90e7/MANIFEST.in000077500000000000000000000000541414641727000210250ustar00rootroot00000000000000include LICENSE README.md setup.cfg VERSION nickw444-nsw-fuel-api-client-a0f90e7/Pipfile000066400000000000000000000003051414641727000205760ustar00rootroot00000000000000[[source]] url = "https://pypi.org/simple" verify_ssl = true name = "pypi" [packages] [dev-packages] flake8 = "*" codecov = "*" mypy = "*" types-requests = "*" [requires] python_version = "3.9" nickw444-nsw-fuel-api-client-a0f90e7/Pipfile.lock000066400000000000000000000304241414641727000215320ustar00rootroot00000000000000{ "_meta": { "hash": { "sha256": "2954b9b9ffdc4315dfbf13969438e2aaf4270404981ad8be26825f03c819779a" }, "pipfile-spec": 6, "requires": { "python_version": "3.9" }, "sources": [ { "name": "pypi", "url": "https://pypi.org/simple", "verify_ssl": true } ] }, "default": {}, "develop": { "certifi": { "hashes": [ "sha256:2bbf76fd432960138b3ef6dda3dde0544f27cbf8546c458e60baf371917ba9ee", "sha256:50b1e4f8446b06f41be7dd6338db18e0990601dce795c2b1686458aa7e8fa7d8" ], "version": "==2021.5.30" }, "charset-normalizer": { "hashes": [ "sha256:88fce3fa5b1a84fdcb3f603d889f723d1dd89b26059d0123ca435570e848d5e1", "sha256:c46c3ace2d744cfbdebceaa3c19ae691f53ae621b39fd7570f59d14fb7f2fd12" ], "markers": "python_version >= '3'", "version": "==2.0.3" }, "codecov": { "hashes": [ "sha256:6cde272454009d27355f9434f4e49f238c0273b216beda8472a65dc4957f473b", "sha256:ba8553a82942ce37d4da92b70ffd6d54cf635fc1793ab0a7dc3fecd6ebfb3df8", "sha256:e95901d4350e99fc39c8353efa450050d2446c55bac91d90fcfd2354e19a6aef" ], "index": "pypi", "version": "==2.1.11" }, "coverage": { "hashes": [ "sha256:004d1880bed2d97151facef49f08e255a20ceb6f9432df75f4eef018fdd5a78c", "sha256:01d84219b5cdbfc8122223b39a954820929497a1cb1422824bb86b07b74594b6", "sha256:040af6c32813fa3eae5305d53f18875bedd079960822ef8ec067a66dd8afcd45", "sha256:06191eb60f8d8a5bc046f3799f8a07a2d7aefb9504b0209aff0b47298333302a", "sha256:13034c4409db851670bc9acd836243aeee299949bd5673e11844befcb0149f03", "sha256:13c4ee887eca0f4c5a247b75398d4114c37882658300e153113dafb1d76de529", "sha256:184a47bbe0aa6400ed2d41d8e9ed868b8205046518c52464fde713ea06e3a74a", "sha256:18ba8bbede96a2c3dde7b868de9dcbd55670690af0988713f0603f037848418a", "sha256:1aa846f56c3d49205c952d8318e76ccc2ae23303351d9270ab220004c580cfe2", "sha256:217658ec7187497e3f3ebd901afdca1af062b42cfe3e0dafea4cced3983739f6", "sha256:24d4a7de75446be83244eabbff746d66b9240ae020ced65d060815fac3423759", "sha256:2910f4d36a6a9b4214bb7038d537f015346f413a975d57ca6b43bf23d6563b53", "sha256:2949cad1c5208b8298d5686d5a85b66aae46d73eec2c3e08c817dd3513e5848a", "sha256:2a3859cb82dcbda1cfd3e6f71c27081d18aa251d20a17d87d26d4cd216fb0af4", "sha256:2cafbbb3af0733db200c9b5f798d18953b1a304d3f86a938367de1567f4b5bff", "sha256:2e0d881ad471768bf6e6c2bf905d183543f10098e3b3640fc029509530091502", "sha256:30c77c1dc9f253283e34c27935fded5015f7d1abe83bc7821680ac444eaf7793", "sha256:3487286bc29a5aa4b93a072e9592f22254291ce96a9fbc5251f566b6b7343cdb", "sha256:372da284cfd642d8e08ef606917846fa2ee350f64994bebfbd3afb0040436905", "sha256:41179b8a845742d1eb60449bdb2992196e211341818565abded11cfa90efb821", "sha256:44d654437b8ddd9eee7d1eaee28b7219bec228520ff809af170488fd2fed3e2b", "sha256:4a7697d8cb0f27399b0e393c0b90f0f1e40c82023ea4d45d22bce7032a5d7b81", "sha256:51cb9476a3987c8967ebab3f0fe144819781fca264f57f89760037a2ea191cb0", "sha256:52596d3d0e8bdf3af43db3e9ba8dcdaac724ba7b5ca3f6358529d56f7a166f8b", "sha256:53194af30d5bad77fcba80e23a1441c71abfb3e01192034f8246e0d8f99528f3", "sha256:5fec2d43a2cc6965edc0bb9e83e1e4b557f76f843a77a2496cbe719583ce8184", "sha256:6c90e11318f0d3c436a42409f2749ee1a115cd8b067d7f14c148f1ce5574d701", "sha256:74d881fc777ebb11c63736622b60cb9e4aee5cace591ce274fb69e582a12a61a", "sha256:7501140f755b725495941b43347ba8a2777407fc7f250d4f5a7d2a1050ba8e82", "sha256:796c9c3c79747146ebd278dbe1e5c5c05dd6b10cc3bcb8389dfdf844f3ead638", "sha256:869a64f53488f40fa5b5b9dcb9e9b2962a66a87dab37790f3fcfb5144b996ef5", "sha256:8963a499849a1fc54b35b1c9f162f4108017b2e6db2c46c1bed93a72262ed083", "sha256:8d0a0725ad7c1a0bcd8d1b437e191107d457e2ec1084b9f190630a4fb1af78e6", "sha256:900fbf7759501bc7807fd6638c947d7a831fc9fdf742dc10f02956ff7220fa90", "sha256:92b017ce34b68a7d67bd6d117e6d443a9bf63a2ecf8567bb3d8c6c7bc5014465", "sha256:970284a88b99673ccb2e4e334cfb38a10aab7cd44f7457564d11898a74b62d0a", "sha256:972c85d205b51e30e59525694670de6a8a89691186012535f9d7dbaa230e42c3", "sha256:9a1ef3b66e38ef8618ce5fdc7bea3d9f45f3624e2a66295eea5e57966c85909e", "sha256:af0e781009aaf59e25c5a678122391cb0f345ac0ec272c7961dc5455e1c40066", "sha256:b6d534e4b2ab35c9f93f46229363e17f63c53ad01330df9f2d6bd1187e5eaacf", "sha256:b7895207b4c843c76a25ab8c1e866261bcfe27bfaa20c192de5190121770672b", "sha256:c0891a6a97b09c1f3e073a890514d5012eb256845c451bd48f7968ef939bf4ae", "sha256:c2723d347ab06e7ddad1a58b2a821218239249a9e4365eaff6649d31180c1669", "sha256:d1f8bf7b90ba55699b3a5e44930e93ff0189aa27186e96071fac7dd0d06a1873", "sha256:d1f9ce122f83b2305592c11d64f181b87153fc2c2bbd3bb4a3dde8303cfb1a6b", "sha256:d314ed732c25d29775e84a960c3c60808b682c08d86602ec2c3008e1202e3bb6", "sha256:d636598c8305e1f90b439dbf4f66437de4a5e3c31fdf47ad29542478c8508bbb", "sha256:deee1077aae10d8fa88cb02c845cfba9b62c55e1183f52f6ae6a2df6a2187160", "sha256:ebe78fe9a0e874362175b02371bdfbee64d8edc42a044253ddf4ee7d3c15212c", "sha256:f030f8873312a16414c0d8e1a1ddff2d3235655a2174e3648b4fa66b3f2f1079", "sha256:f0b278ce10936db1a37e6954e15a3730bea96a0997c26d7fee88e6c396c2086d", "sha256:f11642dddbb0253cc8853254301b51390ba0081750a8ac03f20ea8103f0c56b6" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", "version": "==5.5" }, "flake8": { "hashes": [ "sha256:07528381786f2a6237b061f6e96610a4167b226cb926e2aa2b6b1d78057c576b", "sha256:bf8fd333346d844f616e8d47905ef3a3384edae6b4e9beb0c5101e25e3110907" ], "index": "pypi", "version": "==3.9.2" }, "idna": { "hashes": [ "sha256:14475042e284991034cb48e06f6851428fb14c4dc953acd9be9a5e95c7b6dd7a", "sha256:467fbad99067910785144ce333826c71fb0e63a425657295239737f7ecd125f3" ], "markers": "python_version >= '3'", "version": "==3.2" }, "mccabe": { "hashes": [ "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" ], "version": "==0.6.1" }, "mypy": { "hashes": [ "sha256:088cd9c7904b4ad80bec811053272986611b84221835e079be5bcad029e79dd9", "sha256:0aadfb2d3935988ec3815952e44058a3100499f5be5b28c34ac9d79f002a4a9a", "sha256:119bed3832d961f3a880787bf621634ba042cb8dc850a7429f643508eeac97b9", "sha256:1a85e280d4d217150ce8cb1a6dddffd14e753a4e0c3cf90baabb32cefa41b59e", "sha256:3c4b8ca36877fc75339253721f69603a9c7fdb5d4d5a95a1a1b899d8b86a4de2", "sha256:3e382b29f8e0ccf19a2df2b29a167591245df90c0b5a2542249873b5c1d78212", "sha256:42c266ced41b65ed40a282c575705325fa7991af370036d3f134518336636f5b", "sha256:53fd2eb27a8ee2892614370896956af2ff61254c275aaee4c230ae771cadd885", "sha256:704098302473cb31a218f1775a873b376b30b4c18229421e9e9dc8916fd16150", "sha256:7df1ead20c81371ccd6091fa3e2878559b5c4d4caadaf1a484cf88d93ca06703", "sha256:866c41f28cee548475f146aa4d39a51cf3b6a84246969f3759cb3e9c742fc072", "sha256:a155d80ea6cee511a3694b108c4494a39f42de11ee4e61e72bc424c490e46457", "sha256:adaeee09bfde366d2c13fe6093a7df5df83c9a2ba98638c7d76b010694db760e", "sha256:b6fb13123aeef4a3abbcfd7e71773ff3ff1526a7d3dc538f3929a49b42be03f0", "sha256:b94e4b785e304a04ea0828759172a15add27088520dc7e49ceade7834275bedb", "sha256:c0df2d30ed496a08de5daed2a9ea807d07c21ae0ab23acf541ab88c24b26ab97", "sha256:c6c2602dffb74867498f86e6129fd52a2770c48b7cd3ece77ada4fa38f94eba8", "sha256:ceb6e0a6e27fb364fb3853389607cf7eb3a126ad335790fa1e14ed02fba50811", "sha256:d9dd839eb0dc1bbe866a288ba3c1afc33a202015d2ad83b31e875b5905a079b6", "sha256:e4dab234478e3bd3ce83bac4193b2ecd9cf94e720ddd95ce69840273bf44f6de", "sha256:ec4e0cd079db280b6bdabdc807047ff3e199f334050db5cbb91ba3e959a67504", "sha256:ecd2c3fe726758037234c93df7e98deb257fd15c24c9180dacf1ef829da5f921", "sha256:ef565033fa5a958e62796867b1df10c40263ea9ded87164d67572834e57a174d" ], "index": "pypi", "version": "==0.910" }, "mypy-extensions": { "hashes": [ "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d", "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8" ], "version": "==0.4.3" }, "pycodestyle": { "hashes": [ "sha256:514f76d918fcc0b55c6680472f0a37970994e07bbb80725808c17089be302068", "sha256:c389c1d06bf7904078ca03399a4816f974a1d590090fecea0c63ec26ebaf1cef" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.7.0" }, "pyflakes": { "hashes": [ "sha256:7893783d01b8a89811dd72d7dfd4d84ff098e5eed95cfa8905b22bbffe52efc3", "sha256:f5bc8ecabc05bb9d291eb5203d6810b49040f6ff446a756326104746cc00c1db" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.3.1" }, "requests": { "hashes": [ "sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24", "sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", "version": "==2.26.0" }, "toml": { "hashes": [ "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f" ], "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==0.10.2" }, "types-requests": { "hashes": [ "sha256:ee0d0c507210141b7d5b8639cc43eaa726084178775db2a5fb06fbf85c185808", "sha256:fa5c1e5e832ff6193507d8da7e1159281383908ee193a2f4b37bc08140b51844" ], "index": "pypi", "version": "==2.25.0" }, "typing-extensions": { "hashes": [ "sha256:0ac0f89795dd19de6b97debb0c6af1c70987fd80a2d62d1958f7e56fcc31b497", "sha256:50b6f157849174217d0656f99dc82fe932884fb250826c18350e159ec6cdf342", "sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84" ], "version": "==3.10.0.0" }, "urllib3": { "hashes": [ "sha256:39fb8672126159acb139a7718dd10806104dec1e2f0f6c88aab05d17df10c8d4", "sha256:f57b4c16c62fa2760b7e3d97c35b255512fb6b59a259730f36ba32ce9f8e342f" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", "version": "==1.26.6" } } } nickw444-nsw-fuel-api-client-a0f90e7/README.md000077500000000000000000000007241414641727000205520ustar00rootroot00000000000000nsw-fuel-api-client ============================================ [![](https://travis-ci.org/nickw444/nsw-fuel-api-client.svg?branch=master)](https://travis-ci.org/nickw444/nsw-fuel-api-client) [![](https://coveralls.io/repos/nickw444/nsw-fuel-api-client/badge.svg)](https://coveralls.io/r/nickw444/nsw-fuel-api-client) [![](https://img.shields.io/pypi/v/nsw-fuel-api-client.svg)](https://pypi.python.org/pypi/nsw-fuel-api-client/) API Client for NSW fuel prices. nickw444-nsw-fuel-api-client-a0f90e7/nsw_fuel/000077500000000000000000000000001414641727000211075ustar00rootroot00000000000000nickw444-nsw-fuel-api-client-a0f90e7/nsw_fuel/__init__.py000077500000000000000000000007171414641727000232300ustar00rootroot00000000000000from .client import FuelCheckClient from .dto import ( AveragePrice, Variance, Station, Period, Price, FuelCheckError, GetFuelPricesResponse, FuelType, GetReferenceDataResponse, SortField, TrendPeriod ) __all__ = ["FuelCheckClient", "AveragePrice", "Variance", "Station", "Period", "Price", "FuelCheckError", "GetFuelPricesResponse", "FuelType", "GetReferenceDataResponse", "SortField", "TrendPeriod"] __version__ = "0.0.0-dev" nickw444-nsw-fuel-api-client-a0f90e7/nsw_fuel/client.py000066400000000000000000000120361414641727000227410ustar00rootroot00000000000000import datetime import requests from typing import List, Optional, NamedTuple, Dict, Any from .dto import ( Price, Station, Variance, AveragePrice, FuelCheckError, GetReferenceDataResponse, GetFuelPricesResponse) API_URL_BASE = 'https://api.onegov.nsw.gov.au/FuelCheckApp/v1/fuel' PriceTrends = NamedTuple('PriceTrends', [ ('variances', List[Variance]), ('average_prices', List[AveragePrice]) ]) StationPrice = NamedTuple('StationPrice', [ ('price', Price), ('station', Station) ]) class FuelCheckClient(): def __init__(self, timeout: Optional[int] = 10) -> None: self._timeout = timeout def _format_dt(self, dt: datetime.datetime) -> str: return dt.strftime('%d/%m/%Y %H:%M:%S') def _get_headers(self) -> Dict[str, Any]: return { 'requesttimestamp': self._format_dt(datetime.datetime.now()) } def get_fuel_prices(self) -> GetFuelPricesResponse: """Fetches fuel prices for all stations.""" response = requests.get( '{}/prices'.format(API_URL_BASE), headers=self._get_headers(), timeout=self._timeout, ) if not response.ok: raise FuelCheckError.create(response) return GetFuelPricesResponse.deserialize(response.json()) def get_fuel_prices_for_station( self, station: int ) -> List[Price]: """Gets the fuel prices for a specific fuel station.""" response = requests.get( '{}/prices/station/{}'.format(API_URL_BASE, station), headers=self._get_headers(), timeout=self._timeout, ) if not response.ok: raise FuelCheckError.create(response) data = response.json() return [Price.deserialize(data) for data in data['prices']] def get_fuel_prices_within_radius( self, latitude: float, longitude: float, radius: int, fuel_type: str, brands: Optional[List[str]] = None ) -> List[StationPrice]: """Gets all the fuel prices within the specified radius.""" if brands is None: brands = [] response = requests.post( '{}/prices/nearby'.format(API_URL_BASE), json={ 'fueltype': fuel_type, 'latitude': latitude, 'longitude': longitude, 'radius': radius, 'brand': brands, }, headers=self._get_headers(), timeout=self._timeout, ) if not response.ok: raise FuelCheckError.create(response) data = response.json() stations = { station['code']: Station.deserialize(station) for station in data['stations'] } station_prices = [] # type: List[StationPrice] for serialized_price in data['prices']: price = Price.deserialize(serialized_price) station_prices.append(StationPrice( price=price, station=stations[price.station_code] )) return station_prices def get_fuel_price_trends(self, latitude: float, longitude: float, fuel_types: List[str]) -> PriceTrends: """Gets the fuel price trends for the given location and fuel types.""" response = requests.post( '{}/prices/trends/'.format(API_URL_BASE), json={ 'location': { 'latitude': latitude, 'longitude': longitude, }, 'fueltypes': [{'code': type} for type in fuel_types], }, headers=self._get_headers(), timeout=self._timeout, ) if not response.ok: raise FuelCheckError.create(response) data = response.json() return PriceTrends( variances=[ Variance.deserialize(variance) for variance in data['Variances'] ], average_prices=[ AveragePrice.deserialize(avg_price) for avg_price in data['AveragePrices'] ] ) def get_reference_data( self, modified_since: Optional[datetime.datetime] = None ) -> GetReferenceDataResponse: """ Fetches API reference data. :param modified_since: The response will be empty if no changes have been made to the reference data since this timestamp, otherwise all reference data will be returned. """ if modified_since is None: modified_since = datetime.datetime(year=2010, month=1, day=1) response = requests.get( '{}/lovs'.format(API_URL_BASE), headers={ 'if-modified-since': self._format_dt(modified_since), **self._get_headers(), }, timeout=self._timeout, ) if not response.ok: raise FuelCheckError.create(response) # return response.text return GetReferenceDataResponse.deserialize(response.json()) nickw444-nsw-fuel-api-client-a0f90e7/nsw_fuel/dto.py000066400000000000000000000175251414641727000222610ustar00rootroot00000000000000from datetime import datetime from enum import Enum from typing import Optional, List, Any, Dict from requests import Response class Price(object): def __init__(self, fuel_type: str, price: float, last_updated: Optional[datetime], price_unit: Optional[str], station_code: Optional[int]) -> None: self.fuel_type = fuel_type self.price = price self.last_updated = last_updated self.price_unit = price_unit self.station_code = station_code @classmethod def deserialize(cls, data: Dict[str, Any]) -> 'Price': # Stupid API has two different date representations! :O lastupdated = None try: lastupdated = datetime.strptime( data['lastupdated'], '%d/%m/%Y %H:%M:%S') except ValueError: pass try: lastupdated = datetime.strptime( data['lastupdated'], '%Y-%m-%d %H:%M:%S') except ValueError: pass station_code = None # type: Optional[int] if 'stationcode' in data: station_code = int(data['stationcode']) return Price( fuel_type=data['fueltype'], price=data['price'], last_updated=lastupdated, price_unit=data.get('priceunit'), station_code=station_code ) def __repr__(self) -> str: return ''.format( self.fuel_type, self.price) class Station(object): def __init__(self, id: Optional[str], brand: str, code: int, name: str, address: str) -> None: self.id = id self.brand = brand self.code = code self.name = name self.address = address @classmethod def deserialize(cls, data: Dict[str, Any]) -> 'Station': return Station( id=data.get('stationid'), brand=data['brand'], code=int(data['code']), name=data['name'], address=data['address'] ) def __repr__(self) -> str: return ''.format( self.id, self.code, self.brand, self.name) class Period(Enum): DAY = 'Day' MONTH = 'Month' YEAR = 'Year' WEEK = 'Week' class Variance(object): def __init__(self, fuel_type: str, period: Period, price: float) -> None: self.fuel_type = fuel_type self.period = period self.price = price @classmethod def deserialize(cls, data: Dict[str, Any]) -> 'Variance': return Variance( fuel_type=data['Code'], period=Period(data['Period']), price=data['Price'], ) def __repr__(self) -> str: return ''.format( self.fuel_type, self.period, self.price, ) class AveragePrice(object): def __init__(self, fuel_type: str, period: Period, price: float, captured: datetime) -> None: self.fuel_type = fuel_type self.period = period self.price = price self.captured = captured @classmethod def deserialize(cls, data: Dict[str, Any]) -> 'AveragePrice': period = Period(data['Period']) captured_raw = data['Captured'] if period in [Period.DAY, Period.WEEK, Period.MONTH]: captured = datetime.strptime(captured_raw, '%Y-%m-%d') elif period == Period.YEAR: captured = datetime.strptime(captured_raw, '%B %Y') else: captured = captured_raw return AveragePrice( fuel_type=data['Code'], period=period, price=data['Price'], captured=captured, ) def __repr__(self) -> str: return ('').format( self.fuel_type, self.period, self.price, self.captured ) class FuelType(object): def __init__(self, code: str, name: str) -> None: self.code = code self.name = name @classmethod def deserialize(cls, data: Dict[str, Any]) -> 'FuelType': return FuelType( code=data['code'], name=data['name'] ) class TrendPeriod(object): def __init__(self, period: str, description: str) -> None: self.period = period self.description = description @classmethod def deserialize(cls, data: Dict[str, Any]) -> 'TrendPeriod': return TrendPeriod( period=data['period'], description=data['description'] ) class SortField(object): def __init__(self, code: str, name: str) -> None: self.code = code self.name = name @classmethod def deserialize(cls, data: Dict[str, Any]) -> 'SortField': return SortField( code=data['code'], name=data['name'] ) class GetReferenceDataResponse(object): def __init__(self, stations: List[Station], brands: List[str], fuel_types: List[FuelType], trend_periods: List[TrendPeriod], sort_fields: List[SortField]) -> None: self.stations = stations self.brands = brands self.fuel_types = fuel_types self.trend_periods = trend_periods self.sort_fields = sort_fields @classmethod def deserialize(cls, data: Dict[str, Any]) -> 'GetReferenceDataResponse': stations = [Station.deserialize(x) for x in data['stations']['items']] brands = [x['name'] for x in data['brands']['items']] fuel_types = [FuelType.deserialize(x) for x in data['fueltypes']['items']] trend_periods = [TrendPeriod.deserialize(x) for x in data['trendperiods']['items']] sort_fields = [SortField.deserialize(x) for x in data['sortfields']['items']] return GetReferenceDataResponse( stations=stations, brands=brands, fuel_types=fuel_types, trend_periods=trend_periods, sort_fields=sort_fields ) def __repr__(self) -> str: return ('>').format( len(self.stations) ) class GetFuelPricesResponse(object): def __init__(self, stations: List[Station], prices: List[Price]) -> None: self.stations = stations self.prices = prices @classmethod def deserialize(cls, data: Dict[str, Any]) -> 'GetFuelPricesResponse': stations = [Station.deserialize(x) for x in data['stations']] prices = [Price.deserialize(x) for x in data['prices']] return GetFuelPricesResponse( stations=stations, prices=prices ) class FuelCheckError(Exception): def __init__(self, error_code: Optional[str] = None, description: Optional[str] = None) -> None: super(FuelCheckError, self).__init__(description) self.error_code = error_code @classmethod def create(cls, response: Response) -> 'FuelCheckError': error_code = None description = response.text try: data = response.json() if 'errorDetails' in data: error_details = data['errorDetails'] if type(error_details) == list and len(error_details) > 0: error_details = error_details[0] error_code = error_details.get('code') description = error_details.get('description') elif type(error_details) == dict: error_code = error_details.get('code') description = error_details.get('message') # type: ignore except ValueError: pass return FuelCheckError( error_code=error_code, description=description ) nickw444-nsw-fuel-api-client-a0f90e7/nsw_fuel_tests/000077500000000000000000000000001414641727000223315ustar00rootroot00000000000000nickw444-nsw-fuel-api-client-a0f90e7/nsw_fuel_tests/__init__.py000066400000000000000000000002431414641727000244410ustar00rootroot00000000000000from .integration import FuelCheckClientIntegrationTest from .unit import FuelCheckClientTest __all__ = ['FuelCheckClientTest', 'FuelCheckClientIntegrationTest'] nickw444-nsw-fuel-api-client-a0f90e7/nsw_fuel_tests/fixtures/000077500000000000000000000000001414641727000242025ustar00rootroot00000000000000nickw444-nsw-fuel-api-client-a0f90e7/nsw_fuel_tests/fixtures/all_prices.json000066400000000000000000000022271414641727000272150ustar00rootroot00000000000000{ "stations": [ { "brand": "Cool Fuel Brand", "code": "1", "name": "Cool Fuel Brand Hurstville", "address": "123 Fake Street, Hurstville", "location": { "latitude": -33.00000, "longitude": 151.00000 } }, { "brand": "Fake Fuel Brand", "code": "2", "name": "Fake Fuel Brand Kogarah", "address": "123 Fake Street, Kogarah", "location": { "latitude": -31.00000, "longitude": 152.00000 } } ], "prices": [ { "stationcode": "1", "fueltype": "DL", "price": 157.9, "lastupdated": "29/05/2018 23:28:20" }, { "stationcode": "1", "fueltype": "E10", "price": 156.4, "lastupdated": "29/05/2018 07:18:17" }, { "stationcode": "1", "fueltype": "P95", "price": 174.9, "lastupdated": "07/06/2018 08:05:19" }, { "stationcode": "2", "fueltype": "P95", "price": 175.9, "lastupdated": "23/05/2018 21:51:23" }, { "stationcode": "2", "fueltype": "P98", "price": 182.9, "lastupdated": "23/05/2018 21:51:23" } ] } nickw444-nsw-fuel-api-client-a0f90e7/nsw_fuel_tests/fixtures/lovs.json000066400000000000000000000024361414641727000260650ustar00rootroot00000000000000{ "brands": { "items": [ { "name": "Cool Fuel Brand" }, { "name": "Fake Fuel Brand" } ] }, "fueltypes": { "items": [ { "code": "E10", "name": "Ethanol 94" }, { "code": "U91", "name": "Unleaded 91" } ] }, "stations": { "items": [ { "brand": "Cool Fuel Brand", "code": "1", "name": "Cool Fuel Brand Hurstville", "address": "123 Fake Street, Hurstville", "location": { "latitude": -33.00000, "longitude": 151.00000 } }, { "brand": "Fake Fuel Brand", "code": "2", "name": "Fake Fuel Brand Kogarah", "address": "123 Fake Street, Kogarah", "location": { "latitude": -31.00000, "longitude": 152.00000 } } ] }, "trendperiods": { "items": [ { "period": "Day", "description": "Description for day" }, { "period": "Month", "description": "Description for month" } ] }, "sortfields": { "items": [ { "code": "Sort 1", "name": "Sort field 1" }, { "code": "Sort 2", "name": "Sort field 2" } ] } } nickw444-nsw-fuel-api-client-a0f90e7/nsw_fuel_tests/integration.py000066400000000000000000000014571414641727000252350ustar00rootroot00000000000000import unittest from nsw_fuel import FuelCheckClient class FuelCheckClientIntegrationTest(unittest.TestCase): def setUp(self): self.client = FuelCheckClient() def test_get_reference_data(self) -> None: response = self.client.get_reference_data() self.assertGreater(len(response.stations), 1500) def test_get_fuel_prices(self) -> None: response = self.client.get_fuel_prices() self.assertGreater(len(response.stations), 1500) self.assertGreater(len(response.prices), 1500) def test_get_fuel_prices_for_station(self) -> None: response = self.client.get_reference_data() station_id = response.stations[0].code response = self.client.get_fuel_prices_for_station(station_id) self.assertGreaterEqual(len(response), 1) nickw444-nsw-fuel-api-client-a0f90e7/nsw_fuel_tests/unit.py000077500000000000000000000272521414641727000236750ustar00rootroot00000000000000import datetime import json import os import unittest from requests_mock import Mocker from nsw_fuel import FuelCheckClient, Period, FuelCheckError from nsw_fuel.client import API_URL_BASE class FuelCheckClientTest(unittest.TestCase): def test_construction(self) -> None: FuelCheckClient() @Mocker() def test_get_fuel_prices(self, m: Mocker) -> None: fixture_path = os.path.join(os.path.dirname(__file__), 'fixtures/all_prices.json') with open(fixture_path) as fixture: m.get( '{}/prices'.format(API_URL_BASE), json=json.load(fixture) ) client = FuelCheckClient() response = client.get_fuel_prices() self.assertEqual(len(response.stations), 2) self.assertEqual(len(response.prices), 5) self.assertEqual(response.stations[0].name, 'Cool Fuel Brand Hurstville') self.assertEqual(response.stations[1].name, 'Fake Fuel Brand Kogarah') self.assertEqual(response.prices[0].fuel_type, 'DL') self.assertEqual(response.prices[1].fuel_type, 'E10') self.assertEqual(response.prices[1].station_code, 1) self.assertEqual(response.prices[3].fuel_type, 'P95') self.assertEqual(response.prices[3].station_code, 2) @Mocker() def test_get_fuel_prices_server_error(self, m: Mocker) -> None: m.get( '{}/prices'.format(API_URL_BASE), status_code=500, text='Internal Server Error.', ) client = FuelCheckClient() with self.assertRaises(FuelCheckError) as cm: client.get_fuel_prices() self.assertEqual(str(cm.exception), 'Internal Server Error.') @Mocker() def test_get_fuel_prices_for_station(self, m: Mocker) -> None: m.get('{}/prices/station/100'.format(API_URL_BASE), json={ 'prices': [ { 'fueltype': 'E10', 'price': 146.9, 'lastupdated': '02/06/2018 02:03:04', }, { 'fueltype': 'P95', 'price': 150.0, 'lastupdated': '02/06/2018 02:03:04', } ] }) client = FuelCheckClient() result = client.get_fuel_prices_for_station(100) self.assertEqual(len(result), 2) self.assertEqual(result[0].fuel_type, 'E10') self.assertEqual(result[0].price, 146.9) self.assertEqual(result[0].last_updated, datetime.datetime( day=2, month=6, year=2018, hour=2, minute=3, second=4, )) @Mocker() def test_get_fuel_prices_within_radius(self, m: Mocker) -> None: m.post('{}/prices/nearby'.format(API_URL_BASE), json={ 'stations': [ { 'stationid': 'SAAAAAA', 'brandid': 'BAAAAAA', 'brand': 'Cool Fuel Brand', 'code': 678, 'name': 'Cool Fuel Brand Luxembourg', 'address': '123 Fake Street', 'location': {}, }, { 'stationid': 'SAAAAAB', 'brandid': 'BAAAAAB', 'brand': 'Fake Fuel Brand', 'code': 679, 'name': 'Fake Fuel Brand Luxembourg', 'address': '123 Fake Street', 'location': {}, }, { 'stationid': 'SAAAAAB', 'brandid': 'BAAAAAB', 'brand': 'Fake Fuel Brand2', 'code': 880, 'name': 'Fake Fuel Brand2 Luxembourg', 'address': '123 Fake Street', 'location': {}, }, ], 'prices': [ { 'stationcode': 678, 'fueltype': 'P95', 'price': 150.9, 'priceunit': 'litre', 'description': None, 'lastupdated': '2018-06-02 00:46:31' }, { 'stationcode': 678, 'fueltype': 'P95', 'price': 130.9, 'priceunit': 'litre', 'description': None, 'lastupdated': '2018-06-02 00:46:31' }, { 'stationcode': 880, 'fueltype': 'P95', 'price': 155.9, 'priceunit': 'litre', 'description': None, 'lastupdated': '2018-06-02 00:46:31' } ], }) client = FuelCheckClient() result = client.get_fuel_prices_within_radius( longitude=151.0, latitude=-33.0, radius=10, fuel_type='E10', ) self.assertEqual(len(result), 3) self.assertEqual(result[0].station.code, 678) self.assertEqual(result[0].price.price, 150.9) @Mocker() def test_get_fuel_price_trends(self, m: Mocker) -> None: m.post('{}/prices/trends/'.format(API_URL_BASE), json={ 'Variances': [ {'Code': 'E10', 'Period': 'Day', 'Price': 150.0}, {'Code': 'E10', 'Period': 'Week', 'Price': 151.0}, {'Code': 'E10', 'Period': 'Month', 'Price': 152.0}, {'Code': 'E10', 'Period': 'Year', 'Price': 153.0}, {'Code': 'P95', 'Period': 'Day', 'Price': 150.0}, {'Code': 'P95', 'Period': 'Week', 'Price': 151.0}, {'Code': 'P95', 'Period': 'Month', 'Price': 152.0}, {'Code': 'P95', 'Period': 'Year', 'Price': 153.0}, ], 'AveragePrices': [ {'Code': 'E10', 'Period': 'Day', 'Price': 150.0, 'Captured': '2018-06-02'}, {'Code': 'E10', 'Period': 'Year', 'Price': 151.0, 'Captured': 'October 2017'} ], }) client = FuelCheckClient() result = client.get_fuel_price_trends( longitude=151.0, latitude=-33.0, fuel_types=['E10', 'P95'] ) self.assertEqual(len(result.variances), 8) self.assertEqual(result.variances[0].price, 150.0) self.assertEqual(result.variances[0].period, Period.DAY) self.assertEqual(result.variances[0].fuel_type, 'E10') self.assertEqual(len(result.average_prices), 2) self.assertEqual(result.average_prices[0].fuel_type, 'E10') self.assertEqual(result.average_prices[0].period, Period.DAY) self.assertEqual(result.average_prices[0].captured, datetime.datetime(year=2018, month=6, day=2)) self.assertEqual(result.average_prices[0].price, 150.0) self.assertEqual(result.average_prices[1].period, Period.YEAR) self.assertEqual(result.average_prices[1].captured, datetime.datetime(year=2017, month=10, day=1)) @Mocker() def test_get_fuel_prices_for_station_client_error(self, m: Mocker) -> None: m.get( '{}/prices/station/21199'.format(API_URL_BASE), status_code=400, json={ "errorDetails": [ { "code": "E0014", "description": "Invalid service station code \"21199\"" } ] } ) client = FuelCheckClient() with self.assertRaises(FuelCheckError) as cm: client.get_fuel_prices_for_station(21199) self.assertEqual(str(cm.exception), 'Invalid service station code "21199"') @Mocker() def test_get_fuel_prices_for_station_server_error(self, m: Mocker) -> None: m.get( '{}/prices/station/21199'.format(API_URL_BASE), status_code=500, text='Internal Server Error.', ) client = FuelCheckClient() with self.assertRaises(FuelCheckError) as cm: client.get_fuel_prices_for_station(21199) self.assertEqual(str(cm.exception), 'Internal Server Error.') @Mocker() def test_get_fuel_prices_within_radius_server_error(self, m: Mocker) -> None: m.post( '{}/prices/nearby'.format(API_URL_BASE), status_code=500, text='Internal Server Error.', ) client = FuelCheckClient() with self.assertRaises(FuelCheckError) as cm: client.get_fuel_prices_within_radius( longitude=151.0, latitude=-33.0, radius=10, fuel_type='E10', ) self.assertEqual(str(cm.exception), 'Internal Server Error.') @Mocker() def test_get_fuel_price_trends_server_error(self, m: Mocker) -> None: m.post( '{}/prices/trends/'.format(API_URL_BASE), status_code=500, text='Internal Server Error.', ) client = FuelCheckClient() with self.assertRaises(FuelCheckError) as cm: client.get_fuel_price_trends( longitude=151.0, latitude=-33.0, fuel_types=['E10', 'P95'] ) self.assertEqual(str(cm.exception), 'Internal Server Error.') @Mocker() def test_get_reference_data(self, m: Mocker) -> None: fixture_path = os.path.join(os.path.dirname(__file__), 'fixtures/lovs.json') with open(fixture_path) as fixture: m.get( '{}/lovs'.format(API_URL_BASE), json=json.load(fixture) ) client = FuelCheckClient() response = client.get_reference_data() self.assertEqual(len(response.brands), 2) self.assertEqual(len(response.fuel_types), 2) self.assertEqual(len(response.stations), 2) self.assertEqual(len(response.trend_periods), 2) self.assertEqual(len(response.sort_fields), 2) self.assertEqual(response.brands[0], 'Cool Fuel Brand') self.assertEqual(response.fuel_types[0].code, 'E10') self.assertEqual(response.fuel_types[0].name, 'Ethanol 94') self.assertEqual(response.stations[0].name, 'Cool Fuel Brand Hurstville') self.assertEqual(response.trend_periods[0].period, 'Day') self.assertEqual(response.trend_periods[0].description, 'Description for day') self.assertEqual(response.sort_fields[0].code, 'Sort 1') self.assertEqual(response.sort_fields[0].name, 'Sort field 1') @Mocker() def test_get_reference_data_client_error(self, m: Mocker) -> None: m.get( '{}/lovs'.format(API_URL_BASE), status_code=400, json={ "errorDetails": { "code": "-2146233033", "message": "String was not recognized as a valid DateTime." } }) client = FuelCheckClient() with self.assertRaises(FuelCheckError) as cm: client.get_reference_data() self.assertEqual( str(cm.exception), 'String was not recognized as a valid DateTime.' ) @Mocker() def test_get_reference_data_server_error(self, m: Mocker) -> None: m.get( '{}/lovs'.format(API_URL_BASE), status_code=500, text='Internal Server Error.', ) client = FuelCheckClient() with self.assertRaises(FuelCheckError) as cm: client.get_reference_data() self.assertEqual(str(cm.exception), 'Internal Server Error.') nickw444-nsw-fuel-api-client-a0f90e7/requirements-docs.txt000077500000000000000000000000311414641727000234740ustar00rootroot00000000000000. sphinxcontrib-napoleon nickw444-nsw-fuel-api-client-a0f90e7/setup.cfg000077500000000000000000000001161414641727000211070ustar00rootroot00000000000000[metadata] description-file = README.rst version = attr: nsw_fuel.__version__ nickw444-nsw-fuel-api-client-a0f90e7/setup.py000066400000000000000000000013551414641727000210030ustar00rootroot00000000000000import os from setuptools import setup readme_path = os.path.join(os.path.dirname( os.path.abspath(__file__)), 'README.md', ) long_description = open(readme_path).read() setup( name='nsw-fuel-api-client', packages=['nsw_fuel'], author="Nick Whyte", author_email='nick@nickwhyte.com', description="API Client for NSW Government Fuel", long_description=long_description, long_description_content_type='text/markdown', url='https://github.com/nickw444/nsw-fuel-api-client', zip_safe=False, install_requires=['requests'], classifiers=[ 'Intended Audience :: Developers', 'Programming Language :: Python', ], test_suite="nsw_fuel_tests", tests_require=['requests-mock'] )