From 5e318e263eed68695be9b532451a646b33b773da Mon Sep 17 00:00:00 2001 From: Daniel McKnight Date: Fri, 12 Jan 2024 13:02:51 -0800 Subject: [PATCH 01/17] Initial implementation with proxy endpoints partially implemented Includes accessor methods for MQ Transactional requests Includes API request/response schemas for backend services --- .github/workflows/license_tests.yml | 10 + .github/workflows/propose_release.yml | 29 + .github/workflows/publish_release.yml | 12 + .github/workflows/publish_test_build.yml | 19 + .github/workflows/unit_tests.yml | 31 + LICENSE.md | 21 + README.md | 5 + diana_services_api/__init__.py | 25 + diana_services_api/__main__.py | 38 + diana_services_api/app.py | 61 + diana_services_api/mq_service_api.py | 90 + diana_services_api/schema/__init__.py | 25 + diana_services_api/schema/api_requests.py | 113 ++ diana_services_api/schema/api_responses.py | 1961 ++++++++++++++++++++ diana_services_api/version.py | 29 + requirements/requirements.txt | 5 + requirements/test_requirements.txt | 2 + setup.py | 82 + 18 files changed, 2558 insertions(+) create mode 100644 .github/workflows/license_tests.yml create mode 100644 .github/workflows/propose_release.yml create mode 100644 .github/workflows/publish_release.yml create mode 100644 .github/workflows/publish_test_build.yml create mode 100644 .github/workflows/unit_tests.yml create mode 100644 LICENSE.md create mode 100644 README.md create mode 100644 diana_services_api/__init__.py create mode 100644 diana_services_api/__main__.py create mode 100644 diana_services_api/app.py create mode 100644 diana_services_api/mq_service_api.py create mode 100644 diana_services_api/schema/__init__.py create mode 100644 diana_services_api/schema/api_requests.py create mode 100644 diana_services_api/schema/api_responses.py create mode 100644 diana_services_api/version.py create mode 100644 requirements/requirements.txt create mode 100644 requirements/test_requirements.txt create mode 100644 setup.py diff --git a/.github/workflows/license_tests.yml b/.github/workflows/license_tests.yml new file mode 100644 index 0000000..dcf543d --- /dev/null +++ b/.github/workflows/license_tests.yml @@ -0,0 +1,10 @@ +name: Run License Tests +on: + push: + workflow_dispatch: + pull_request: + branches: + - master +jobs: + license_tests: + uses: neongeckocom/.github/.github/workflows/license_tests.yml@master \ No newline at end of file diff --git a/.github/workflows/propose_release.yml b/.github/workflows/propose_release.yml new file mode 100644 index 0000000..eeb6de6 --- /dev/null +++ b/.github/workflows/propose_release.yml @@ -0,0 +1,29 @@ +name: Propose Stable Release +on: + workflow_dispatch: + inputs: + release_type: + type: choice + description: Release Type + options: + - patch + - minor + - major +jobs: + update_version: + uses: neongeckocom/.github/.github/workflows/propose_semver_release.yml@master + with: + branch: dev + release_type: ${{ inputs.release_type }} + update_changelog: True + version_file: "neon_diana_utils/version.py" + on_version_change: "scripts/sync_chart_app_version.py" + pull_changes: + uses: neongeckocom/.github/.github/workflows/pull_master.yml@master + needs: update_version + with: + pr_reviewer: neonreviewers + pr_assignee: ${{ github.actor }} + pr_draft: false + pr_title: ${{ needs.update_version.outputs.version }} + pr_body: ${{ needs.update_version.outputs.changelog }} \ No newline at end of file diff --git a/.github/workflows/publish_release.yml b/.github/workflows/publish_release.yml new file mode 100644 index 0000000..96a0357 --- /dev/null +++ b/.github/workflows/publish_release.yml @@ -0,0 +1,12 @@ +# This workflow will generate a release distribution and upload it to PyPI + +name: Publish Build and GitHub Release +on: + push: + branches: + - master + +jobs: + build_and_publish_pypi_and_release: + uses: neongeckocom/.github/.github/workflows/publish_stable_release.yml@master + secrets: inherit \ No newline at end of file diff --git a/.github/workflows/publish_test_build.yml b/.github/workflows/publish_test_build.yml new file mode 100644 index 0000000..d871d93 --- /dev/null +++ b/.github/workflows/publish_test_build.yml @@ -0,0 +1,19 @@ +# This workflow will generate a distribution and upload it to PyPI + +name: Publish Alpha Build +on: + push: + branches: + - dev + paths-ignore: + - 'neon_diana_utils/version.py' + - '**/Chart.yaml' + +jobs: + publish_alpha_release: + uses: neongeckocom/.github/.github/workflows/publish_alpha_release.yml@master + secrets: inherit + with: + version_file: "neon_diana_utils/version.py" + publish_prerelease: true + on_version_change: "scripts/sync_chart_app_version.py" \ No newline at end of file diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml new file mode 100644 index 0000000..987913c --- /dev/null +++ b/.github/workflows/unit_tests.yml @@ -0,0 +1,31 @@ +name: Run Unit Tests +on: + pull_request: + workflow_dispatch: + +jobs: + unit_tests: + strategy: + matrix: + python-version: [ 3.7, 3.8, 3.9, "3.10" ] + timeout-minutes: 15 + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Set up python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -e . + pip install -r requirements/test_requirements.txt + - name: Run Tests + run: | + pytest tests --doctest-modules --junitxml=tests/diana-test-results.xml + - name: Upload test results + uses: actions/upload-artifact@v2 + with: + name: utils-test-results + path: tests/diana-test-results.xml diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..307a15b --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,21 @@ +# NEON AI (TM) SOFTWARE, Software Development Kit & Application Development System +# All trademark and other rights reserved by their respective owners +# Copyright 2008-2021 Neongecko.com Inc. +# BSD-3 + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +following conditions are met: +1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +disclaimer. +2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following +disclaimer in the documentation and/or other materials provided with the distribution. +3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products +derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..00064e9 --- /dev/null +++ b/README.md @@ -0,0 +1,5 @@ +# Diana Services API +This package provides an HTTP front-end for accessing services in a +[Neon DIANA](https://github.com/NeonGeckoCom/neon-diana-utils) deployment. This +should generally be hosted as part of a Diana deployment to safely expose services +via HTTP. diff --git a/diana_services_api/__init__.py b/diana_services_api/__init__.py new file mode 100644 index 0000000..d782cbb --- /dev/null +++ b/diana_services_api/__init__.py @@ -0,0 +1,25 @@ +# NEON AI (TM) SOFTWARE, Software Development Kit & Application Development System +# All trademark and other rights reserved by their respective owners +# Copyright 2008-2021 Neongecko.com Inc. +# BSD-3 +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from this +# software without specific prior written permission. +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +# OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/diana_services_api/__main__.py b/diana_services_api/__main__.py new file mode 100644 index 0000000..951b1d8 --- /dev/null +++ b/diana_services_api/__main__.py @@ -0,0 +1,38 @@ +# NEON AI (TM) SOFTWARE, Software Development Kit & Application Development System +# All trademark and other rights reserved by their respective owners +# Copyright 2008-2021 Neongecko.com Inc. +# BSD-3 +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from this +# software without specific prior written permission. +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +# OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +import uvicorn + +from diana_services_api.app import create_app + + +def main(): + app = create_app() + uvicorn.run(app, host="0.0.0.0", port=8080) + + +if __name__ == "__main__": + main() diff --git a/diana_services_api/app.py b/diana_services_api/app.py new file mode 100644 index 0000000..779eb20 --- /dev/null +++ b/diana_services_api/app.py @@ -0,0 +1,61 @@ +# NEON AI (TM) SOFTWARE, Software Development Kit & Application Development System +# All trademark and other rights reserved by their respective owners +# Copyright 2008-2021 Neongecko.com Inc. +# BSD-3 +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from this +# software without specific prior written permission. +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +# OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +from fastapi import FastAPI +from typing import Union + +from diana_services_api.schema.api_requests import * +from diana_services_api.schema.api_responses import * +from diana_services_api.mq_service_api import MQServiceManager + + +def create_app(): + mq_connector = MQServiceManager() + app = FastAPI() + + @app.post("/proxy/weather") + async def api_proxy_weather(query: WeatherAPIRequest) -> WeatherAPIOnecallResponse: + return mq_connector.query_api_proxy("open_weather_map", dict(query)) + + @app.post("/proxy/stock/symbol") + async def api_proxy_stock_symbol(query: StockAPISymbolRequest) -> StockAPISearchResponse: + return mq_connector.query_api_proxy("alpha_vantage", + {**dict(query), + **{"api": "symbol"}}) + + @app.post("/proxy/stock/quote") + async def api_proxy_stock_quote(query: StockAPIQuoteRequest) -> StockAPIQuoteResponse: + return mq_connector.query_api_proxy("alpha_vantage", + {**dict(query), **{"api": "quote"}}) + + @app.post("/proxy/geolocation/geocode") + async def api_proxy_geolocation(query: GeoAPIRequest) -> GeoAPIGeocodeResponse: + return mq_connector.query_api_proxy("map_maker", dict(query)) + + @app.post("/proxy/geolocation/reverse") + async def api_proxy_geolocation(query: GeoAPIReverseRequest) -> GeoAPIReverseResponse: + return mq_connector.query_api_proxy("map_maker", dict(query)) + return app diff --git a/diana_services_api/mq_service_api.py b/diana_services_api/mq_service_api.py new file mode 100644 index 0000000..619df02 --- /dev/null +++ b/diana_services_api/mq_service_api.py @@ -0,0 +1,90 @@ +# NEON AI (TM) SOFTWARE, Software Development Kit & Application Development System +# All trademark and other rights reserved by their respective owners +# Copyright 2008-2021 Neongecko.com Inc. +# BSD-3 +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from this +# software without specific prior written permission. +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +# OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +import json + +from fastapi import HTTPException + +from neon_mq_connector.utils.client_utils import send_mq_request + + +class APIError(HTTPException): + """ + Exception class representing errors in getting responses from the MQ API + """ + + +class MQServiceManager: + def _validate_api_proxy_response(self, response: dict): + if response['status_code'] == 200: + resp = json.loads(response['content']) + if isinstance(resp, dict): + return resp + # Reverse Geocode API returns a list; reformat that to a dict + if isinstance(resp, list): + return {**resp.pop(0), + **{"alternate_results": resp}} + code = response['status_code'] if response['status_code'] > 200 else 500 + raise APIError(status_code=code, detail=response['content']) + + def query_api_proxy(self, service_name: str, query_params: dict, + timeout: int = 10): + query_params['service'] = service_name + response = send_mq_request("/neon_api", query_params, "neon_api_input", + "neon_api_output", timeout) + return self._validate_api_proxy_response(response) + + + def parse_ccl_script(self, script_text: str, metadata: dict = None, + timeout: int = 30): + response = send_mq_request("/neon_script_parser", + {"text": script_text, "metadata": metadata}, + "neon_script_parser_input", + "neon_script_parser_output", timeout) + return response + + def get_stt(self, b64_audio: str, lang: str, timeout: int = 20): + request_data = {"msg_type": "neon.get_stt", + "data": {"audio_data": b64_audio, + "utterances": [""], # TODO: Compat + "lang": lang}, + "context": {"source": "diana_services_api"}} + response = send_mq_request("/neon_chat_api", request_data, + "neon_chat_api_request", timeout=timeout) + return response + + def get_tts(self, string: str, lang: str, gender: str, timeout: int = 20): + request_data = {"msg_type": "neon.get_tts", + "data": {"text": string, + "utterance": "", # TODO: Compat + "speaker": {"name": "Neon", + "gender": gender, + "lang": lang}, + "lang": lang}, + "context": {"source": "diana_services_api"}} + response = send_mq_request("/neon_chat_api", request_data, + "neon_chat_api_request", timeout=timeout) + return response diff --git a/diana_services_api/schema/__init__.py b/diana_services_api/schema/__init__.py new file mode 100644 index 0000000..d782cbb --- /dev/null +++ b/diana_services_api/schema/__init__.py @@ -0,0 +1,25 @@ +# NEON AI (TM) SOFTWARE, Software Development Kit & Application Development System +# All trademark and other rights reserved by their respective owners +# Copyright 2008-2021 Neongecko.com Inc. +# BSD-3 +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from this +# software without specific prior written permission. +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +# OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/diana_services_api/schema/api_requests.py b/diana_services_api/schema/api_requests.py new file mode 100644 index 0000000..17baf58 --- /dev/null +++ b/diana_services_api/schema/api_requests.py @@ -0,0 +1,113 @@ +# NEON AI (TM) SOFTWARE, Software Development Kit & Application Development System +# All trademark and other rights reserved by their respective owners +# Copyright 2008-2021 Neongecko.com Inc. +# BSD-3 +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from this +# software without specific prior written permission. +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +# OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +from typing import Optional + +from pydantic import BaseModel + + +class WeatherAPIRequest(BaseModel): + api: str = "onecall" + lat: float + lon: float + unit: str = "metric" + + model_config = { + "json_schema_extra": { + "examples": [{ + "api": "onecall", + "lat": 47.6815, + "lon": -122.2087, + "unit": "imperial", + }, { + "api": "onecall", + "lat": 47.6815, + "lon": -122.2087, + "unit": "metric", + }]}} + + +class StockAPISymbolRequest(BaseModel): + company: Optional[str] = None + model_config = { + "json_schema_extra": { + "examples": [{"company": "microsoft"}]}} + + +class StockAPIQuoteRequest(BaseModel): + symbol: Optional[str] = None + model_config = { + "json_schema_extra": { + "examples": [{"symbol": "GOOG"}]}} + + +class GeoAPIRequest(BaseModel): + address: str + + model_config = { + "json_schema_extra": { + "examples": [{ + "address": "1100 Bellevue Way NE Bellevue, WA" + }]}} + + +class GeoAPIReverseRequest(BaseModel): + lat: float + lon: float + model_config = { + "json_schema_extra": { + "examples": [{ + "lat": 47.6815, + "lon": -122.2087, + }]}} + + +class WolframAlphaAPIRequest(BaseModel): + api: str + unit: str = "metric" + lat: float + lon: float + query: str + + +class ParseScriptRequest(BaseModel): + script: str + + +class GetCouponsRequest(BaseModel): + pass + + +class SendEmailRequest(BaseModel): + recipient: str + subject: str + body: str + attachments: Optional[str] = None + + +class UploadMetricRequest(BaseModel): + metric_name: str + metric_data: str diff --git a/diana_services_api/schema/api_responses.py b/diana_services_api/schema/api_responses.py new file mode 100644 index 0000000..fe68b05 --- /dev/null +++ b/diana_services_api/schema/api_responses.py @@ -0,0 +1,1961 @@ +# NEON AI (TM) SOFTWARE, Software Development Kit & Application Development System +# All trademark and other rights reserved by their respective owners +# Copyright 2008-2021 Neongecko.com Inc. +# BSD-3 +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from this +# software without specific prior written permission. +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +# OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +from typing import Dict, List, Any, Optional +from pydantic import BaseModel, Field + + +class WeatherAPIOnecallResponse(BaseModel): + lat: float + lon: float + timezone: str + timezone_offset: int + current: Dict[str, Any] + minutely: List[dict] + hourly: List[dict] + daily: List[dict] + + model_config = { + "extra": "allow", + "json_schema_extra": { + "examples": [ + { + "lat": 47.6815, + "lon": -122.2087, + "timezone": "America/Los_Angeles", + "timezone_offset": -28800, + "current": { + "dt": 1705080482, + "sunrise": 1705074869, + "sunset": 1705106347, + "temp": 18.36, + "feels_like": 9.05, + "pressure": 1022, + "humidity": 66, + "dew_point": 9.93, + "uvi": 0.18, + "clouds": 75, + "visibility": 10000, + "wind_speed": 7, + "wind_deg": 360, + "wind_gust": 14, + "weather": [ + { + "id": 803, + "main": "Clouds", + "description": "broken clouds", + "icon": "04d" + } + ] + }, + "minutely": [ + { + "dt": 1705080540, + "precipitation": 0 + }, + { + "dt": 1705080600, + "precipitation": 0 + }, + { + "dt": 1705080660, + "precipitation": 0 + }, + { + "dt": 1705080720, + "precipitation": 0 + }, + { + "dt": 1705080780, + "precipitation": 0 + }, + { + "dt": 1705080840, + "precipitation": 0 + }, + { + "dt": 1705080900, + "precipitation": 0 + }, + { + "dt": 1705080960, + "precipitation": 0 + }, + { + "dt": 1705081020, + "precipitation": 0 + }, + { + "dt": 1705081080, + "precipitation": 0 + }, + { + "dt": 1705081140, + "precipitation": 0 + }, + { + "dt": 1705081200, + "precipitation": 0 + }, + { + "dt": 1705081260, + "precipitation": 0 + }, + { + "dt": 1705081320, + "precipitation": 0 + }, + { + "dt": 1705081380, + "precipitation": 0 + }, + { + "dt": 1705081440, + "precipitation": 0 + }, + { + "dt": 1705081500, + "precipitation": 0 + }, + { + "dt": 1705081560, + "precipitation": 0 + }, + { + "dt": 1705081620, + "precipitation": 0 + }, + { + "dt": 1705081680, + "precipitation": 0 + }, + { + "dt": 1705081740, + "precipitation": 0 + }, + { + "dt": 1705081800, + "precipitation": 0 + }, + { + "dt": 1705081860, + "precipitation": 0 + }, + { + "dt": 1705081920, + "precipitation": 0 + }, + { + "dt": 1705081980, + "precipitation": 0 + }, + { + "dt": 1705082040, + "precipitation": 0 + }, + { + "dt": 1705082100, + "precipitation": 0 + }, + { + "dt": 1705082160, + "precipitation": 0 + }, + { + "dt": 1705082220, + "precipitation": 0 + }, + { + "dt": 1705082280, + "precipitation": 0 + }, + { + "dt": 1705082340, + "precipitation": 0 + }, + { + "dt": 1705082400, + "precipitation": 0 + }, + { + "dt": 1705082460, + "precipitation": 0 + }, + { + "dt": 1705082520, + "precipitation": 0 + }, + { + "dt": 1705082580, + "precipitation": 0 + }, + { + "dt": 1705082640, + "precipitation": 0 + }, + { + "dt": 1705082700, + "precipitation": 0 + }, + { + "dt": 1705082760, + "precipitation": 0 + }, + { + "dt": 1705082820, + "precipitation": 0 + }, + { + "dt": 1705082880, + "precipitation": 0 + }, + { + "dt": 1705082940, + "precipitation": 0 + }, + { + "dt": 1705083000, + "precipitation": 0 + }, + { + "dt": 1705083060, + "precipitation": 0 + }, + { + "dt": 1705083120, + "precipitation": 0 + }, + { + "dt": 1705083180, + "precipitation": 0 + }, + { + "dt": 1705083240, + "precipitation": 0 + }, + { + "dt": 1705083300, + "precipitation": 0 + }, + { + "dt": 1705083360, + "precipitation": 0 + }, + { + "dt": 1705083420, + "precipitation": 0 + }, + { + "dt": 1705083480, + "precipitation": 0 + }, + { + "dt": 1705083540, + "precipitation": 0 + }, + { + "dt": 1705083600, + "precipitation": 0 + }, + { + "dt": 1705083660, + "precipitation": 0 + }, + { + "dt": 1705083720, + "precipitation": 0 + }, + { + "dt": 1705083780, + "precipitation": 0 + }, + { + "dt": 1705083840, + "precipitation": 0 + }, + { + "dt": 1705083900, + "precipitation": 0 + }, + { + "dt": 1705083960, + "precipitation": 0 + }, + { + "dt": 1705084020, + "precipitation": 0 + }, + { + "dt": 1705084080, + "precipitation": 0 + } + ], + "hourly": [ + { + "dt": 1705078800, + "temp": 18.36, + "feels_like": 8.4, + "pressure": 1022, + "humidity": 66, + "dew_point": 9.93, + "uvi": 0.18, + "clouds": 75, + "visibility": 10000, + "wind_speed": 7.78, + "wind_deg": 345, + "wind_gust": 11.9, + "weather": [ + { + "id": 803, + "main": "Clouds", + "description": "broken clouds", + "icon": "04d" + } + ], + "pop": 0 + }, + { + "dt": 1705082400, + "temp": 18.37, + "feels_like": 8.19, + "pressure": 1022, + "humidity": 63, + "dew_point": 9.01, + "uvi": 0.41, + "clouds": 72, + "visibility": 10000, + "wind_speed": 8.08, + "wind_deg": 346, + "wind_gust": 10.65, + "weather": [ + { + "id": 803, + "main": "Clouds", + "description": "broken clouds", + "icon": "04d" + } + ], + "pop": 0 + }, + { + "dt": 1705086000, + "temp": 18.91, + "feels_like": 9.01, + "pressure": 1023, + "humidity": 60, + "dew_point": 8.56, + "uvi": 0.66, + "clouds": 49, + "visibility": 10000, + "wind_speed": 7.87, + "wind_deg": 348, + "wind_gust": 9.31, + "weather": [ + { + "id": 802, + "main": "Clouds", + "description": "scattered clouds", + "icon": "03d" + } + ], + "pop": 0 + }, + { + "dt": 1705089600, + "temp": 19.74, + "feels_like": 9.99, + "pressure": 1024, + "humidity": 55, + "dew_point": 7.65, + "uvi": 0.78, + "clouds": 35, + "visibility": 10000, + "wind_speed": 7.92, + "wind_deg": 348, + "wind_gust": 8.97, + "weather": [ + { + "id": 802, + "main": "Clouds", + "description": "scattered clouds", + "icon": "03d" + } + ], + "pop": 0 + }, + { + "dt": 1705093200, + "temp": 20.77, + "feels_like": 11.23, + "pressure": 1024, + "humidity": 50, + "dew_point": 6.73, + "uvi": 0.72, + "clouds": 21, + "visibility": 10000, + "wind_speed": 7.92, + "wind_deg": 350, + "wind_gust": 8.52, + "weather": [ + { + "id": 801, + "main": "Clouds", + "description": "few clouds", + "icon": "02d" + } + ], + "pop": 0 + }, + { + "dt": 1705096800, + "temp": 21.67, + "feels_like": 12.24, + "pressure": 1024, + "humidity": 47, + "dew_point": 3.74, + "uvi": 0.52, + "clouds": 7, + "visibility": 10000, + "wind_speed": 8.03, + "wind_deg": 353, + "wind_gust": 8.97, + "weather": [ + { + "id": 800, + "main": "Clear", + "description": "clear sky", + "icon": "01d" + } + ], + "pop": 0 + }, + { + "dt": 1705100400, + "temp": 21.54, + "feels_like": 12.09, + "pressure": 1024, + "humidity": 48, + "dew_point": 3.83, + "uvi": 0.26, + "clouds": 9, + "visibility": 10000, + "wind_speed": 8.03, + "wind_deg": 352, + "wind_gust": 9.19, + "weather": [ + { + "id": 800, + "main": "Clear", + "description": "clear sky", + "icon": "01d" + } + ], + "pop": 0 + }, + { + "dt": 1705104000, + "temp": 20.66, + "feels_like": 11.16, + "pressure": 1024, + "humidity": 51, + "dew_point": 4.28, + "uvi": 0, + "clouds": 13, + "visibility": 10000, + "wind_speed": 7.85, + "wind_deg": 350, + "wind_gust": 10.54, + "weather": [ + { + "id": 801, + "main": "Clouds", + "description": "few clouds", + "icon": "02d" + } + ], + "pop": 0 + }, + { + "dt": 1705107600, + "temp": 19.15, + "feels_like": 10.02, + "pressure": 1024, + "humidity": 54, + "dew_point": 4.1, + "uvi": 0, + "clouds": 27, + "visibility": 10000, + "wind_speed": 6.98, + "wind_deg": 351, + "wind_gust": 12.08, + "weather": [ + { + "id": 802, + "main": "Clouds", + "description": "scattered clouds", + "icon": "03n" + } + ], + "pop": 0 + }, + { + "dt": 1705111200, + "temp": 18.68, + "feels_like": 10.29, + "pressure": 1024, + "humidity": 55, + "dew_point": 4.19, + "uvi": 0, + "clouds": 33, + "visibility": 10000, + "wind_speed": 6.08, + "wind_deg": 4, + "wind_gust": 13.02, + "weather": [ + { + "id": 802, + "main": "Clouds", + "description": "scattered clouds", + "icon": "03n" + } + ], + "pop": 0 + }, + { + "dt": 1705114800, + "temp": 18.39, + "feels_like": 11.01, + "pressure": 1024, + "humidity": 56, + "dew_point": 4.15, + "uvi": 0, + "clouds": 36, + "visibility": 10000, + "wind_speed": 5.08, + "wind_deg": 21, + "wind_gust": 11.43, + "weather": [ + { + "id": 802, + "main": "Clouds", + "description": "scattered clouds", + "icon": "03n" + } + ], + "pop": 0 + }, + { + "dt": 1705118400, + "temp": 17.67, + "feels_like": 9.88, + "pressure": 1024, + "humidity": 57, + "dew_point": 3.92, + "uvi": 0, + "clouds": 46, + "visibility": 10000, + "wind_speed": 5.32, + "wind_deg": 52, + "wind_gust": 10.56, + "weather": [ + { + "id": 802, + "main": "Clouds", + "description": "scattered clouds", + "icon": "03n" + } + ], + "pop": 0 + }, + { + "dt": 1705122000, + "temp": 16.07, + "feels_like": 7.65, + "pressure": 1024, + "humidity": 57, + "dew_point": 2.17, + "uvi": 0, + "clouds": 56, + "visibility": 10000, + "wind_speed": 5.64, + "wind_deg": 72, + "wind_gust": 8.61, + "weather": [ + { + "id": 803, + "main": "Clouds", + "description": "broken clouds", + "icon": "04n" + } + ], + "pop": 0 + }, + { + "dt": 1705125600, + "temp": 14.31, + "feels_like": 4.93, + "pressure": 1024, + "humidity": 54, + "dew_point": -0.89, + "uvi": 0, + "clouds": 64, + "visibility": 10000, + "wind_speed": 6.22, + "wind_deg": 85, + "wind_gust": 9.31, + "weather": [ + { + "id": 803, + "main": "Clouds", + "description": "broken clouds", + "icon": "04n" + } + ], + "pop": 0 + }, + { + "dt": 1705129200, + "temp": 13.59, + "feels_like": 4.37, + "pressure": 1024, + "humidity": 49, + "dew_point": -3.46, + "uvi": 0, + "clouds": 99, + "visibility": 10000, + "wind_speed": 5.93, + "wind_deg": 83, + "wind_gust": 9.01, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04n" + } + ], + "pop": 0 + }, + { + "dt": 1705132800, + "temp": 13.01, + "feels_like": 3.31, + "pressure": 1023, + "humidity": 46, + "dew_point": -5.46, + "uvi": 0, + "clouds": 100, + "visibility": 10000, + "wind_speed": 6.29, + "wind_deg": 83, + "wind_gust": 9.71, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04n" + } + ], + "pop": 0 + }, + { + "dt": 1705136400, + "temp": 12.36, + "feels_like": 2.5, + "pressure": 1023, + "humidity": 45, + "dew_point": -6.5, + "uvi": 0, + "clouds": 100, + "visibility": 10000, + "wind_speed": 6.33, + "wind_deg": 77, + "wind_gust": 9.84, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04n" + } + ], + "pop": 0 + }, + { + "dt": 1705140000, + "temp": 11.8, + "feels_like": 2.59, + "pressure": 1022, + "humidity": 45, + "dew_point": -7.19, + "uvi": 0, + "clouds": 100, + "visibility": 10000, + "wind_speed": 5.64, + "wind_deg": 75, + "wind_gust": 8.79, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04n" + } + ], + "pop": 0 + }, + { + "dt": 1705143600, + "temp": 11.39, + "feels_like": 1.42, + "pressure": 1022, + "humidity": 45, + "dew_point": -7.78, + "uvi": 0, + "clouds": 100, + "visibility": 10000, + "wind_speed": 6.24, + "wind_deg": 75, + "wind_gust": 9.78, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04n" + } + ], + "pop": 0 + }, + { + "dt": 1705147200, + "temp": 10.71, + "feels_like": 0.73, + "pressure": 1021, + "humidity": 45, + "dew_point": -8.55, + "uvi": 0, + "clouds": 100, + "visibility": 10000, + "wind_speed": 6.13, + "wind_deg": 77, + "wind_gust": 9.84, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04n" + } + ], + "pop": 0 + }, + { + "dt": 1705150800, + "temp": 10.4, + "feels_like": -0.04, + "pressure": 1020, + "humidity": 44, + "dew_point": -9.15, + "uvi": 0, + "clouds": 100, + "visibility": 10000, + "wind_speed": 6.51, + "wind_deg": 77, + "wind_gust": 10.33, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04n" + } + ], + "pop": 0 + }, + { + "dt": 1705154400, + "temp": 9.97, + "feels_like": -0.2, + "pressure": 1019, + "humidity": 44, + "dew_point": -9.89, + "uvi": 0, + "clouds": 100, + "visibility": 10000, + "wind_speed": 6.17, + "wind_deg": 73, + "wind_gust": 10, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04n" + } + ], + "pop": 0 + }, + { + "dt": 1705158000, + "temp": 9.88, + "feels_like": 0.64, + "pressure": 1019, + "humidity": 43, + "dew_point": -10.3, + "uvi": 0, + "clouds": 100, + "visibility": 10000, + "wind_speed": 5.35, + "wind_deg": 69, + "wind_gust": 8.28, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04n" + } + ], + "pop": 0 + }, + { + "dt": 1705161600, + "temp": 9.39, + "feels_like": 0.1, + "pressure": 1018, + "humidity": 44, + "dew_point": -10.37, + "uvi": 0, + "clouds": 100, + "visibility": 10000, + "wind_speed": 5.32, + "wind_deg": 70, + "wind_gust": 8.9, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04d" + } + ], + "pop": 0 + }, + { + "dt": 1705165200, + "temp": 10.35, + "feels_like": 1.8, + "pressure": 1017, + "humidity": 43, + "dew_point": -9.99, + "uvi": 0.16, + "clouds": 100, + "visibility": 10000, + "wind_speed": 4.88, + "wind_deg": 72, + "wind_gust": 8.32, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04d" + } + ], + "pop": 0 + }, + { + "dt": 1705168800, + "temp": 12.11, + "feels_like": 1.4, + "pressure": 1016, + "humidity": 40, + "dew_point": -9.29, + "uvi": 0.36, + "clouds": 100, + "visibility": 10000, + "wind_speed": 7.14, + "wind_deg": 61, + "wind_gust": 11.32, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04d" + } + ], + "pop": 0 + }, + { + "dt": 1705172400, + "temp": 14.52, + "feels_like": 5.34, + "pressure": 1014, + "humidity": 38, + "dew_point": -8.27, + "uvi": 0.5, + "clouds": 100, + "visibility": 10000, + "wind_speed": 6.06, + "wind_deg": 59, + "wind_gust": 10.13, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04d" + } + ], + "pop": 0 + }, + { + "dt": 1705176000, + "temp": 17.11, + "feels_like": 9.99, + "pressure": 1014, + "humidity": 36, + "dew_point": -6.72, + "uvi": 0.62, + "clouds": 100, + "visibility": 10000, + "wind_speed": 4.68, + "wind_deg": 54, + "wind_gust": 7.96, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04d" + } + ], + "pop": 0 + }, + { + "dt": 1705179600, + "temp": 18.7, + "feels_like": 10.42, + "pressure": 1012, + "humidity": 36, + "dew_point": -5.01, + "uvi": 0.58, + "clouds": 100, + "visibility": 10000, + "wind_speed": 5.97, + "wind_deg": 53, + "wind_gust": 9.91, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04d" + } + ], + "pop": 0 + }, + { + "dt": 1705183200, + "temp": 19.42, + "feels_like": 11.01, + "pressure": 1011, + "humidity": 37, + "dew_point": -3.53, + "uvi": 0.47, + "clouds": 100, + "visibility": 10000, + "wind_speed": 6.24, + "wind_deg": 51, + "wind_gust": 9.86, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04d" + } + ], + "pop": 0 + }, + { + "dt": 1705186800, + "temp": 19.15, + "feels_like": 10.17, + "pressure": 1010, + "humidity": 40, + "dew_point": -2.43, + "uvi": 0.25, + "clouds": 100, + "visibility": 10000, + "wind_speed": 6.82, + "wind_deg": 45, + "wind_gust": 11.01, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04d" + } + ], + "pop": 0 + }, + { + "dt": 1705190400, + "temp": 18.07, + "feels_like": 9.61, + "pressure": 1010, + "humidity": 43, + "dew_point": -1.57, + "uvi": 0, + "clouds": 100, + "visibility": 10000, + "wind_speed": 6.04, + "wind_deg": 50, + "wind_gust": 10.87, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04d" + } + ], + "pop": 0 + }, + { + "dt": 1705194000, + "temp": 16.54, + "feels_like": 8.38, + "pressure": 1011, + "humidity": 48, + "dew_point": -0.98, + "uvi": 0, + "clouds": 100, + "visibility": 10000, + "wind_speed": 5.46, + "wind_deg": 57, + "wind_gust": 9.82, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04n" + } + ], + "pop": 0 + }, + { + "dt": 1705197600, + "temp": 15.53, + "feels_like": 7.2, + "pressure": 1011, + "humidity": 49, + "dew_point": -1.44, + "uvi": 0, + "clouds": 100, + "visibility": 10000, + "wind_speed": 5.46, + "wind_deg": 62, + "wind_gust": 10.71, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04n" + } + ], + "pop": 0 + }, + { + "dt": 1705201200, + "temp": 14.92, + "feels_like": 6.87, + "pressure": 1012, + "humidity": 48, + "dew_point": -2.6, + "uvi": 0, + "clouds": 100, + "visibility": 10000, + "wind_speed": 5.12, + "wind_deg": 76, + "wind_gust": 9.4, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04n" + } + ], + "pop": 0 + }, + { + "dt": 1705204800, + "temp": 14.5, + "feels_like": 6.87, + "pressure": 1013, + "humidity": 48, + "dew_point": -2.85, + "uvi": 0, + "clouds": 100, + "visibility": 10000, + "wind_speed": 4.72, + "wind_deg": 91, + "wind_gust": 8.97, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04n" + } + ], + "pop": 0 + }, + { + "dt": 1705208400, + "temp": 14.47, + "feels_like": 6.42, + "pressure": 1014, + "humidity": 48, + "dew_point": -2.92, + "uvi": 0, + "clouds": 100, + "visibility": 10000, + "wind_speed": 5.06, + "wind_deg": 94, + "wind_gust": 9.13, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04n" + } + ], + "pop": 0 + }, + { + "dt": 1705212000, + "temp": 14.56, + "feels_like": 7.95, + "pressure": 1014, + "humidity": 47, + "dew_point": -3.28, + "uvi": 0, + "clouds": 100, + "visibility": 10000, + "wind_speed": 3.98, + "wind_deg": 82, + "wind_gust": 7.27, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04n" + } + ], + "pop": 0 + }, + { + "dt": 1705215600, + "temp": 14.86, + "feels_like": 8.28, + "pressure": 1015, + "humidity": 46, + "dew_point": -3.77, + "uvi": 0, + "clouds": 100, + "visibility": 10000, + "wind_speed": 4, + "wind_deg": 82, + "wind_gust": 7.18, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04n" + } + ], + "pop": 0 + }, + { + "dt": 1705219200, + "temp": 15.08, + "feels_like": 9.28, + "pressure": 1015, + "humidity": 45, + "dew_point": -3.69, + "uvi": 0, + "clouds": 100, + "visibility": 10000, + "wind_speed": 3.51, + "wind_deg": 90, + "wind_gust": 6.53, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04n" + } + ], + "pop": 0 + }, + { + "dt": 1705222800, + "temp": 15.01, + "feels_like": 15.01, + "pressure": 1016, + "humidity": 47, + "dew_point": -3.01, + "uvi": 0, + "clouds": 100, + "visibility": 10000, + "wind_speed": 2.48, + "wind_deg": 82, + "wind_gust": 4.59, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04n" + } + ], + "pop": 0 + }, + { + "dt": 1705226400, + "temp": 15.22, + "feels_like": 9.55, + "pressure": 1017, + "humidity": 48, + "dew_point": -2.34, + "uvi": 0, + "clouds": 100, + "visibility": 10000, + "wind_speed": 3.44, + "wind_deg": 103, + "wind_gust": 6.24, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04n" + } + ], + "pop": 0 + }, + { + "dt": 1705230000, + "temp": 15.21, + "feels_like": 15.21, + "pressure": 1018, + "humidity": 50, + "dew_point": -1.41, + "uvi": 0, + "clouds": 99, + "visibility": 10000, + "wind_speed": 1.7, + "wind_deg": 101, + "wind_gust": 2.93, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04n" + } + ], + "pop": 0 + }, + { + "dt": 1705233600, + "temp": 15.19, + "feels_like": 15.19, + "pressure": 1019, + "humidity": 52, + "dew_point": -0.51, + "uvi": 0, + "clouds": 89, + "visibility": 10000, + "wind_speed": 2.98, + "wind_deg": 105, + "wind_gust": 4.88, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04n" + } + ], + "pop": 0 + }, + { + "dt": 1705237200, + "temp": 15.71, + "feels_like": 15.71, + "pressure": 1019, + "humidity": 53, + "dew_point": 0.32, + "uvi": 0, + "clouds": 96, + "visibility": 10000, + "wind_speed": 2.13, + "wind_deg": 118, + "wind_gust": 4.07, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04n" + } + ], + "pop": 0 + }, + { + "dt": 1705240800, + "temp": 15.26, + "feels_like": 10.09, + "pressure": 1020, + "humidity": 56, + "dew_point": 1.04, + "uvi": 0, + "clouds": 56, + "visibility": 10000, + "wind_speed": 3.15, + "wind_deg": 130, + "wind_gust": 5.44, + "weather": [ + { + "id": 803, + "main": "Clouds", + "description": "broken clouds", + "icon": "04n" + } + ], + "pop": 0 + }, + { + "dt": 1705244400, + "temp": 15.26, + "feels_like": 15.26, + "pressure": 1021, + "humidity": 57, + "dew_point": 1.53, + "uvi": 0, + "clouds": 40, + "visibility": 10000, + "wind_speed": 2.68, + "wind_deg": 122, + "wind_gust": 4.05, + "weather": [ + { + "id": 802, + "main": "Clouds", + "description": "scattered clouds", + "icon": "03n" + } + ], + "pop": 0 + }, + { + "dt": 1705248000, + "temp": 15.31, + "feels_like": 15.31, + "pressure": 1022, + "humidity": 58, + "dew_point": 1.83, + "uvi": 0, + "clouds": 32, + "visibility": 10000, + "wind_speed": 2.93, + "wind_deg": 129, + "wind_gust": 4.32, + "weather": [ + { + "id": 802, + "main": "Clouds", + "description": "scattered clouds", + "icon": "03d" + } + ], + "pop": 0 + } + ], + "daily": [ + { + "dt": 1705089600, + "sunrise": 1705074869, + "sunset": 1705106347, + "moonrise": 1705080180, + "moonset": 1705111980, + "moon_phase": 0.05, + "temp": { + "day": 19.74, + "min": 13.59, + "max": 25.88, + "night": 13.59, + "eve": 18.68, + "morn": 18.7 + }, + "feels_like": { + "day": 9.99, + "night": 4.37, + "eve": 10.29, + "morn": 8.56 + }, + "pressure": 1024, + "humidity": 55, + "dew_point": 7.65, + "wind_speed": 9.62, + "wind_deg": 328, + "wind_gust": 17.87, + "weather": [ + { + "id": 802, + "main": "Clouds", + "description": "scattered clouds", + "icon": "03d" + } + ], + "clouds": 35, + "pop": 0.43, + "uvi": 0.78 + }, + { + "dt": 1705176000, + "sunrise": 1705161237, + "sunset": 1705192824, + "moonrise": 1705168260, + "moonset": 1705203660, + "moon_phase": 0.09, + "temp": { + "day": 17.11, + "min": 9.39, + "max": 19.42, + "night": 14.86, + "eve": 15.53, + "morn": 9.97 + }, + "feels_like": { + "day": 9.99, + "night": 8.28, + "eve": 7.2, + "morn": -0.2 + }, + "pressure": 1014, + "humidity": 36, + "dew_point": -6.72, + "wind_speed": 7.14, + "wind_deg": 61, + "wind_gust": 11.32, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04d" + } + ], + "clouds": 100, + "pop": 0, + "uvi": 0.62 + }, + { + "dt": 1705262400, + "sunrise": 1705247602, + "sunset": 1705279304, + "moonrise": 1705255920, + "moonset": 1705295220, + "moon_phase": 0.13, + "temp": { + "day": 26.42, + "min": 15.01, + "max": 27.97, + "night": 22.41, + "eve": 23.58, + "morn": 15.26 + }, + "feels_like": { + "day": 26.42, + "night": 22.41, + "eve": 23.58, + "morn": 10.09 + }, + "pressure": 1023, + "humidity": 40, + "dew_point": 4.87, + "wind_speed": 3.51, + "wind_deg": 90, + "wind_gust": 6.53, + "weather": [ + { + "id": 803, + "main": "Clouds", + "description": "broken clouds", + "icon": "04d" + } + ], + "clouds": 65, + "pop": 0, + "uvi": 0.73 + }, + { + "dt": 1705348800, + "sunrise": 1705333965, + "sunset": 1705365784, + "moonrise": 1705343460, + "moonset": 1705386480, + "moon_phase": 0.16, + "temp": { + "day": 32.43, + "min": 20.66, + "max": 32.43, + "night": 24.03, + "eve": 25.74, + "morn": 20.66 + }, + "feels_like": { + "day": 32.43, + "night": 20.05, + "eve": 21.63, + "morn": 20.66 + }, + "pressure": 1029, + "humidity": 41, + "dew_point": 11.03, + "wind_speed": 3.4, + "wind_deg": 80, + "wind_gust": 6.11, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04d" + } + ], + "clouds": 98, + "pop": 0, + "uvi": 0.9 + }, + { + "dt": 1705435200, + "sunrise": 1705420325, + "sunset": 1705452267, + "moonrise": 1705430880, + "moonset": 1705477740, + "moon_phase": 0.2, + "temp": { + "day": 35.67, + "min": 23.2, + "max": 35.67, + "night": 31.51, + "eve": 29.66, + "morn": 23.4 + }, + "feels_like": { + "day": 35.67, + "night": 28.67, + "eve": 26.51, + "morn": 23.4 + }, + "pressure": 1026, + "humidity": 42, + "dew_point": 14.59, + "wind_speed": 3.11, + "wind_deg": 132, + "wind_gust": 5.21, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04d" + } + ], + "clouds": 97, + "pop": 0, + "uvi": 1 + }, + { + "dt": 1705521600, + "sunrise": 1705506683, + "sunset": 1705538750, + "moonrise": 1705518360, + "moonset": 0, + "moon_phase": 0.25, + "temp": { + "day": 36.27, + "min": 33.31, + "max": 36.27, + "night": 36.23, + "eve": 36, + "morn": 33.44 + }, + "feels_like": { + "day": 36.27, + "night": 36.23, + "eve": 36, + "morn": 33.44 + }, + "pressure": 1024, + "humidity": 95, + "dew_point": 34.72, + "wind_speed": 2.42, + "wind_deg": 155, + "wind_gust": 2.62, + "weather": [ + { + "id": 500, + "main": "Rain", + "description": "light rain", + "icon": "10d" + } + ], + "clouds": 100, + "pop": 0.9, + "rain": 2.34, + "uvi": 1 + }, + { + "dt": 1705608000, + "sunrise": 1705593038, + "sunset": 1705625235, + "moonrise": 1705605900, + "moonset": 1705568880, + "moon_phase": 0.27, + "temp": { + "day": 39.09, + "min": 36.5, + "max": 40.23, + "night": 40.23, + "eve": 39.79, + "morn": 37.13 + }, + "feels_like": { + "day": 39.09, + "night": 40.23, + "eve": 39.79, + "morn": 37.13 + }, + "pressure": 1024, + "humidity": 98, + "dew_point": 38.34, + "wind_speed": 3.22, + "wind_deg": 29, + "wind_gust": 3.04, + "weather": [ + { + "id": 500, + "main": "Rain", + "description": "light rain", + "icon": "10d" + } + ], + "clouds": 100, + "pop": 0.88, + "rain": 3.3, + "uvi": 1 + }, + { + "dt": 1705694400, + "sunrise": 1705679391, + "sunset": 1705711720, + "moonrise": 1705693620, + "moonset": 1705659960, + "moon_phase": 0.31, + "temp": { + "day": 50.5, + "min": 39.88, + "max": 50.5, + "night": 40.96, + "eve": 44.17, + "morn": 39.88 + }, + "feels_like": { + "day": 49.39, + "night": 38.59, + "eve": 44.17, + "morn": 38.16 + }, + "pressure": 1022, + "humidity": 88, + "dew_point": 46.85, + "wind_speed": 3.83, + "wind_deg": 129, + "wind_gust": 4.29, + "weather": [ + { + "id": 803, + "main": "Clouds", + "description": "broken clouds", + "icon": "04d" + } + ], + "clouds": 63, + "pop": 0, + "uvi": 1 + } + ] + } + ] + } + } + + +class StockAPIQuoteResponse(BaseModel): + global_quote: Dict[str, str] = Field(..., alias="Global Quote") + + model_config = { + "extra": "allow", + "json_schema_extra": { + "examples": [ + { + "Global Quote": { + "01. symbol": "GOOG", + "02. open": "144.8950", + "03. high": "146.6600", + "04. low": "142.2150", + "05. price": "143.6700", + "06. volume": "17471130", + "07. latest trading day": "2024-01-11", + "08. previous close": "143.8000", + "09. change": "-0.1300", + "10. change percent": "-0.0904%" + } + } + ]}} + + +class StockAPISearchResponse(BaseModel): + model_config = { + "extra": "allow", + "json_schema_extra": { + "examples": [ + { + "bestMatches": [ + { + "1. symbol": "MSF0.FRK", + "2. name": "MICROSOFT CORP. CDR", + "3. type": "Equity", + "4. region": "Frankfurt", + "5. marketOpen": "08:00", + "6. marketClose": "20:00", + "7. timezone": "UTC+02", + "8. currency": "EUR", + "9. matchScore": "0.6429" + }, + { + "1. symbol": "MSFT", + "2. name": "Microsoft Corporation", + "3. type": "Equity", + "4. region": "United States", + "5. marketOpen": "09:30", + "6. marketClose": "16:00", + "7. timezone": "UTC-04", + "8. currency": "USD", + "9. matchScore": "0.6154" + }, + { + "1. symbol": "0QYP.LON", + "2. name": "Microsoft Corporation", + "3. type": "Equity", + "4. region": "United Kingdom", + "5. marketOpen": "08:00", + "6. marketClose": "16:30", + "7. timezone": "UTC+01", + "8. currency": "USD", + "9. matchScore": "0.6000" + }, + { + "1. symbol": "MSF.DEX", + "2. name": "Microsoft Corporation", + "3. type": "Equity", + "4. region": "XETRA", + "5. marketOpen": "08:00", + "6. marketClose": "20:00", + "7. timezone": "UTC+02", + "8. currency": "EUR", + "9. matchScore": "0.6000" + }, + { + "1. symbol": "MSF.FRK", + "2. name": "Microsoft Corporation", + "3. type": "Equity", + "4. region": "Frankfurt", + "5. marketOpen": "08:00", + "6. marketClose": "20:00", + "7. timezone": "UTC+02", + "8. currency": "EUR", + "9. matchScore": "0.6000" + }, + { + "1. symbol": "MSFT34.SAO", + "2. name": "Microsoft Corporation", + "3. type": "Equity", + "4. region": "Brazil/Sao Paolo", + "5. marketOpen": "10:00", + "6. marketClose": "17:30", + "7. timezone": "UTC-03", + "8. currency": "BRL", + "9. matchScore": "0.6000" + } + ] + }]}} + + +class GeoAPIGeocodeResponse(BaseModel): + place_id: int + licence: str + osm_type: str + osm_id: int + boundingbox: List[str] + lat: str + lon: str + display_name: str + class_: Optional[str] = Field(..., alias="class") + type_: Optional[str] = Field(..., alias="type") + importance: float + alternate_results: List[dict] + + model_config = { + "extra": "allow", + "json_schema_extra": { + "examples": [ + { + "place_id": 288749081, + "licence": "Data © OpenStreetMap contributors, ODbL 1.0. https://osm.org/copyright", + "osm_type": "node", + "osm_id": 9106438617, + "boundingbox": [ + "47.6204274", + "47.6205274", + "-122.200047", + "-122.199947" + ], + "lat": "47.6204774", + "lon": "-122.199997", + "display_name": "The UPS Store, 1100, Bellevue Way Northeast, Bellevue, King County, Washington, 98004, United States", + "class": "amenity", + "type": "post_office", + "importance": 0.53001, + "alternate_results": [ + { + "place_id": 288749055, + "licence": "Data © OpenStreetMap contributors, ODbL 1.0. https://osm.org/copyright", + "osm_type": "node", + "osm_id": 1987569546, + "boundingbox": [ + "47.6204239", + "47.6205239", + "-122.2001916", + "-122.2000916" + ], + "lat": "47.6204739", + "lon": "-122.2001416", + "display_name": "AAA Cruises and Travel, 1100, Bellevue Way Northeast, Bellevue, King County, Washington, 98004, United States", + "class": "club", + "type": "automobile", + "importance": 0.53001 + }, + { + "place_id": 288749230, + "licence": "Data © OpenStreetMap contributors, ODbL 1.0. https://osm.org/copyright", + "osm_type": "node", + "osm_id": 1987569543, + "boundingbox": [ + "47.620366", + "47.620466", + "-122.201487", + "-122.201387" + ], + "lat": "47.620416", + "lon": "-122.201437", + "display_name": "Adventure Kids Playcare, 1100, Bellevue Way Northeast, Bellevue, King County, Washington, 98004, United States", + "class": "leisure", + "type": "playground", + "importance": 0.53001 + } + ] + }]}} + + +class GeoAPIReverseResponse(BaseModel): + place_id: int + licence: str + osm_type: str + osm_id: int + lat: str + lon: str + display_name: str + address: Dict[str, str] + boundingbox: List[str] + model_config = { + "extra": "allow", + "json_schema_extra": { + "examples": [ + { + "place_id": 288417123, + "licence": "Data © OpenStreetMap contributors, ODbL 1.0. https://osm.org/copyright", + "osm_type": "way", + "osm_id": 325822620, + "lat": "47.68148615", + "lon": "-122.20873015166683", + "display_name": "807, 1st Street, Juanita, Kirkland, King County, Washington, 98033, United States", + "address": { + "house_number": "807", + "road": "1st Street", + "suburb": "Juanita", + "town": "Kirkland", + "county": "King County", + "state": "Washington", + "ISO3166-2-lvl4": "US-WA", + "postcode": "98033", + "country": "United States", + "country_code": "us" + }, + "boundingbox": [ + "47.6814167", + "47.6815308", + "-122.2088759", + "-122.2086379" + ] + }]}} diff --git a/diana_services_api/version.py b/diana_services_api/version.py new file mode 100644 index 0000000..e47fc1c --- /dev/null +++ b/diana_services_api/version.py @@ -0,0 +1,29 @@ +# NEON AI (TM) SOFTWARE, Software Development Kit & Application Framework +# All trademark and other rights reserved by their respective owners +# Copyright 2008-2022 Neongecko.com Inc. +# Contributors: Daniel McKnight, Guy Daniels, Elon Gasper, Richard Leeds, +# Regina Bloomstine, Casimiro Ferreira, Andrii Pernatii, Kirill Hrymailo +# BSD-3 License +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from this +# software without specific prior written permission. +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +# OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +__version__ = "0.0.1a1" diff --git a/requirements/requirements.txt b/requirements/requirements.txt new file mode 100644 index 0000000..6c91f4c --- /dev/null +++ b/requirements/requirements.txt @@ -0,0 +1,5 @@ +pyyaml>=5.4,<7.0 +fastapi~=0.95 +uvicorn~=0.25 +pydantic~=2.5 +neon_mq_connector~=0.7 \ No newline at end of file diff --git a/requirements/test_requirements.txt b/requirements/test_requirements.txt new file mode 100644 index 0000000..68e751a --- /dev/null +++ b/requirements/test_requirements.txt @@ -0,0 +1,2 @@ +pytest +mock \ No newline at end of file diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..192af02 --- /dev/null +++ b/setup.py @@ -0,0 +1,82 @@ +# NEON AI (TM) SOFTWARE, Software Development Kit & Application Framework +# All trademark and other rights reserved by their respective owners +# Copyright 2008-2022 Neongecko.com Inc. +# Contributors: Daniel McKnight, Guy Daniels, Elon Gasper, Richard Leeds, +# Regina Bloomstine, Casimiro Ferreira, Andrii Pernatii, Kirill Hrymailo +# BSD-3 License +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from this +# software without specific prior written permission. +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +# OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +from setuptools import setup, find_packages +from os import getenv, path, walk + +BASE_PATH = path.abspath(path.dirname(__file__)) + + +def get_requirements(requirements_filename: str): + requirements_file = path.join(BASE_PATH, "requirements", requirements_filename) + with open(requirements_file, 'r', encoding='utf-8') as r: + requirements = r.readlines() + requirements = [r.strip() for r in requirements if r.strip() and not r.strip().startswith("#")] + + for i in range(0, len(requirements)): + r = requirements[i] + if "@" in r: + parts = [p.lower() if p.strip().startswith("git+http") else p for p in r.split('@')] + r = "@".join(parts) + if getenv("GITHUB_TOKEN"): + if "github.com" in r: + requirements[i] = r.replace("github.com", f"{getenv('GITHUB_TOKEN')}@github.com") + return requirements + + +with open(path.join(BASE_PATH, "README.md"), "r") as f: + long_description = f.read() + +with open(path.join(BASE_PATH, "diana_services_api", + "version.py"), "r", encoding="utf-8") as v: + for line in v.readlines(): + if line.startswith("__version__"): + if '"' in line: + version = line.split('"')[1] + else: + version = line.split("'")[1] + + +setup( + name='diana-services-api', + version=version, + description='Web API to access Neon DIANA Services', + long_description=long_description, + long_description_content_type="text/markdown", + url='https://github.com/NeonGeckoCom/diana-services-api', + author='NeonGecko', + author_email='developers@neon.ai', + license='BSD-3-Clause', + packages=find_packages(), + install_requires=get_requirements("requirements.txt"), + zip_safe=True, + classifiers=[ + 'Intended Audience :: Developers', + 'Programming Language :: Python :: 3.6', + ] +) From 4fc5fdef26f14c2ea09f01f90260a6faff3b057b Mon Sep 17 00:00:00 2001 From: Daniel McKnight Date: Fri, 12 Jan 2024 16:17:52 -0800 Subject: [PATCH 02/17] Implement basic auth with unit tests --- .github/workflows/unit_tests.yml | 10 +- diana_services_api/app.py | 21 +++-- diana_services_api/auth/__init__.py | 25 +++++ diana_services_api/auth/client_manager.py | 103 +++++++++++++++++++++ diana_services_api/schema/auth_requests.py | 59 ++++++++++++ requirements/requirements.txt | 1 + tests/__init__.py | 25 +++++ tests/test_auth.py | 71 ++++++++++++++ 8 files changed, 300 insertions(+), 15 deletions(-) create mode 100644 diana_services_api/auth/__init__.py create mode 100644 diana_services_api/auth/client_manager.py create mode 100644 diana_services_api/schema/auth_requests.py create mode 100644 tests/__init__.py create mode 100644 tests/test_auth.py diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml index 987913c..53f199d 100644 --- a/.github/workflows/unit_tests.yml +++ b/.github/workflows/unit_tests.yml @@ -19,13 +19,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install -e . - pip install -r requirements/test_requirements.txt + pip install . -r requirements/test_requirements.txt - name: Run Tests run: | - pytest tests --doctest-modules --junitxml=tests/diana-test-results.xml - - name: Upload test results - uses: actions/upload-artifact@v2 - with: - name: utils-test-results - path: tests/diana-test-results.xml + pytest tests diff --git a/diana_services_api/app.py b/diana_services_api/app.py index 779eb20..26b6ba2 100644 --- a/diana_services_api/app.py +++ b/diana_services_api/app.py @@ -24,38 +24,45 @@ # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -from fastapi import FastAPI -from typing import Union +from fastapi import FastAPI, Depends +from diana_services_api.schema.auth_requests import * from diana_services_api.schema.api_requests import * from diana_services_api.schema.api_responses import * from diana_services_api.mq_service_api import MQServiceManager +from diana_services_api.auth.client_manager import ClientManager, JWTBearer def create_app(): mq_connector = MQServiceManager() + client_manager = ClientManager() + jwt_bearer = JWTBearer(client_manager) app = FastAPI() - @app.post("/proxy/weather") + @app.post("/auth/login") + async def check_login(request: AuthenticationRequest) -> AuthenticationResponse: + return client_manager.check_auth_request(**dict(request)) + + @app.post("/proxy/weather", dependencies=[Depends(jwt_bearer)]) async def api_proxy_weather(query: WeatherAPIRequest) -> WeatherAPIOnecallResponse: return mq_connector.query_api_proxy("open_weather_map", dict(query)) - @app.post("/proxy/stock/symbol") + @app.post("/proxy/stock/symbol", dependencies=[Depends(jwt_bearer)]) async def api_proxy_stock_symbol(query: StockAPISymbolRequest) -> StockAPISearchResponse: return mq_connector.query_api_proxy("alpha_vantage", {**dict(query), **{"api": "symbol"}}) - @app.post("/proxy/stock/quote") + @app.post("/proxy/stock/quote", dependencies=[Depends(jwt_bearer)]) async def api_proxy_stock_quote(query: StockAPIQuoteRequest) -> StockAPIQuoteResponse: return mq_connector.query_api_proxy("alpha_vantage", {**dict(query), **{"api": "quote"}}) - @app.post("/proxy/geolocation/geocode") + @app.post("/proxy/geolocation/geocode", dependencies=[Depends(jwt_bearer)]) async def api_proxy_geolocation(query: GeoAPIRequest) -> GeoAPIGeocodeResponse: return mq_connector.query_api_proxy("map_maker", dict(query)) - @app.post("/proxy/geolocation/reverse") + @app.post("/proxy/geolocation/reverse", dependencies=[Depends(jwt_bearer)]) async def api_proxy_geolocation(query: GeoAPIReverseRequest) -> GeoAPIReverseResponse: return mq_connector.query_api_proxy("map_maker", dict(query)) return app diff --git a/diana_services_api/auth/__init__.py b/diana_services_api/auth/__init__.py new file mode 100644 index 0000000..d782cbb --- /dev/null +++ b/diana_services_api/auth/__init__.py @@ -0,0 +1,25 @@ +# NEON AI (TM) SOFTWARE, Software Development Kit & Application Development System +# All trademark and other rights reserved by their respective owners +# Copyright 2008-2021 Neongecko.com Inc. +# BSD-3 +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from this +# software without specific prior written permission. +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +# OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/diana_services_api/auth/client_manager.py b/diana_services_api/auth/client_manager.py new file mode 100644 index 0000000..626f348 --- /dev/null +++ b/diana_services_api/auth/client_manager.py @@ -0,0 +1,103 @@ +# NEON AI (TM) SOFTWARE, Software Development Kit & Application Development System +# All trademark and other rights reserved by their respective owners +# Copyright 2008-2021 Neongecko.com Inc. +# BSD-3 +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from this +# software without specific prior written permission. +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +# OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +import jwt + +from time import time +from typing import Dict, Optional + +from fastapi import Request, HTTPException +from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials +from jwt import DecodeError + + +class ClientManager: + def __init__(self): + self.authorized_clients: Dict[str, dict] = dict() + # TODO: secrets and expirations from config + self._access_token_lifetime = 3600 * 24 # 1 day + self._refresh_token_lifetime = 3600 * 24 * 7 # 1 week + self._access_secret = 'a800445648142061fc238d1f84e96200da87f4f9f784108ac90db8b4391b117b' + self._refresh_secret = '833d369ac73d883123743a44b4a7fe21203cffc956f4c8a99be6e71aafa8e1aa' + self._jwt_algo = "HS256" + + def check_auth_request(self, client_id: str, username: str, + password: Optional[str] = None): + if client_id in self.authorized_clients: + return self.authorized_clients[client_id] + if username != "guest": + # TODO: Validate password here + pass + expiration = time() + self._access_token_lifetime + encode_data = {"client_id": client_id, + "username": username, + "password": password, + "expire": expiration} + token = jwt.encode(encode_data, self._access_secret, self._jwt_algo) + encode_data['expire'] = time() + self._refresh_token_lifetime + refresh = jwt.encode(encode_data, self._refresh_secret, self._jwt_algo) + # TODO: Store refresh token on server to validate refresh requests + auth = {"username": username, + "client_id": client_id, + "access_token": token, + "refresh_token": refresh} + self.authorized_clients[client_id] = auth + return auth + + def validate_auth(self, token: str) -> bool: + try: + auth = jwt.decode(token, self._access_secret, self._jwt_algo) + if auth['expire'] < time(): + self.authorized_clients.pop(auth['client_id'], None) + return False + # Keep track of authorized client connections + self.authorized_clients[auth['client_id']] = auth + return True + except DecodeError: + # Invalid token supplied + pass + return False + + +class JWTBearer(HTTPBearer): + def __init__(self, client_manager: ClientManager): + HTTPBearer.__init__(self) + self.client_manager = client_manager + + async def __call__(self, request: Request): + credentials: HTTPAuthorizationCredentials = \ + await super(JWTBearer, self).__call__(request) + if credentials: + if not credentials.scheme == "Bearer": + raise HTTPException(status_code=403, + detail="Invalid authentication scheme.") + if not self.client_manager.validate_auth(credentials.credentials): + raise HTTPException(status_code=403, + detail="Invalid or expired token.") + return credentials.credentials + else: + raise HTTPException(status_code=403, + detail="Invalid or missing auth credentials.") diff --git a/diana_services_api/schema/auth_requests.py b/diana_services_api/schema/auth_requests.py new file mode 100644 index 0000000..3c58df0 --- /dev/null +++ b/diana_services_api/schema/auth_requests.py @@ -0,0 +1,59 @@ +# NEON AI (TM) SOFTWARE, Software Development Kit & Application Development System +# All trademark and other rights reserved by their respective owners +# Copyright 2008-2021 Neongecko.com Inc. +# BSD-3 +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from this +# software without specific prior written permission. +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +# OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +from typing import Optional +from uuid import uuid4 + +from pydantic import BaseModel + + +class AuthenticationRequest(BaseModel): + username: str = "guest" + password: Optional[str] = None + client_id: str = str(uuid4()) + + model_config = { + "json_schema_extra": { + "examples": [{ + "username": "guest", + "password": "password" + }]}} + + +class AuthenticationResponse(BaseModel): + username: str + client_id: str + access_token: str + refresh_token: str + + model_config = { + "json_schema_extra": { + "examples": [{ + "username": "guest", + "client_id": "be84ae66-f61c-4aac-a9af-b0da364b82b6", + "access_token": "", + "refresh_token": "" + }]}} diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 6c91f4c..24302ce 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -2,4 +2,5 @@ pyyaml>=5.4,<7.0 fastapi~=0.95 uvicorn~=0.25 pydantic~=2.5 +pyjwt~=2.8 neon_mq_connector~=0.7 \ No newline at end of file diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..d782cbb --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1,25 @@ +# NEON AI (TM) SOFTWARE, Software Development Kit & Application Development System +# All trademark and other rights reserved by their respective owners +# Copyright 2008-2021 Neongecko.com Inc. +# BSD-3 +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from this +# software without specific prior written permission. +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +# OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/tests/test_auth.py b/tests/test_auth.py new file mode 100644 index 0000000..823ccd6 --- /dev/null +++ b/tests/test_auth.py @@ -0,0 +1,71 @@ +# NEON AI (TM) SOFTWARE, Software Development Kit & Application Development System +# All trademark and other rights reserved by their respective owners +# Copyright 2008-2021 Neongecko.com Inc. +# BSD-3 +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from this +# software without specific prior written permission. +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +# OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +import unittest +from uuid import uuid4 + + +class TestClientManager(unittest.TestCase): + from diana_services_api.auth.client_manager import ClientManager + client_manager = ClientManager() + + def test_check_auth_request(self): + client_1 = str(uuid4()) + client_2 = str(uuid4()) + request_1 = {"username": "guest", "password": None, + "client_id": client_1} + request_2 = {"username": "guest", "password": None, + "client_id": client_2} + + # Check simple auth + auth_resp_1 = self.client_manager.check_auth_request(**request_1) + self.assertEqual(self.client_manager.authorized_clients[client_1], + auth_resp_1) + self.assertEqual(auth_resp_1['username'], 'guest') + self.assertEqual(auth_resp_1['client_id'], client_1) + + # Check auth from different client + auth_resp_2 = self.client_manager.check_auth_request(**request_2) + self.assertNotEquals(auth_resp_1, auth_resp_2) + self.assertEqual(self.client_manager.authorized_clients[client_2], + auth_resp_2) + self.assertEqual(auth_resp_2['username'], 'guest') + self.assertEqual(auth_resp_2['client_id'], client_2) + + # Check auth already authorized + self.assertEqual(auth_resp_2, + self.client_manager.check_auth_request(**request_2)) + + def test_validate_auth(self): + valid_client = str(uuid4()) + invalid_client = str(uuid4()) + auth_response = self.client_manager.check_auth_request( + username="valid", client_id=valid_client)['jwt_token'] + + self.assertTrue(self.client_manager.validate_auth(auth_response)) + self.assertFalse(self.client_manager.validate_auth(invalid_client)) + self.client_manager.authorized_clients.pop(valid_client) + self.assertFalse(self.client_manager.validate_auth(auth_response)) From 4e80b059cfd95ba24dfa585e920f3e9e52c93873 Mon Sep 17 00:00:00 2001 From: Daniel McKnight Date: Fri, 12 Jan 2024 16:39:39 -0800 Subject: [PATCH 03/17] Implement WolframAlpha API with examples --- diana_services_api/app.py | 5 ++++ diana_services_api/mq_service_api.py | 21 ++++++++----- diana_services_api/schema/api_requests.py | 35 +++++++++++++++++----- diana_services_api/schema/api_responses.py | 15 ++++++++++ 4 files changed, 60 insertions(+), 16 deletions(-) diff --git a/diana_services_api/app.py b/diana_services_api/app.py index 26b6ba2..8258c57 100644 --- a/diana_services_api/app.py +++ b/diana_services_api/app.py @@ -65,4 +65,9 @@ async def api_proxy_geolocation(query: GeoAPIRequest) -> GeoAPIGeocodeResponse: @app.post("/proxy/geolocation/reverse", dependencies=[Depends(jwt_bearer)]) async def api_proxy_geolocation(query: GeoAPIReverseRequest) -> GeoAPIReverseResponse: return mq_connector.query_api_proxy("map_maker", dict(query)) + + @app.post("/proxy/wolframalpha", dependencies=[Depends(jwt_bearer)]) + async def api_proxy_wolframalpha(query: WolframAlphaAPIRequest) -> WolframAlphaAPIResponse: + return mq_connector.query_api_proxy("wolfram_alpha", dict(query)) + return app diff --git a/diana_services_api/mq_service_api.py b/diana_services_api/mq_service_api.py index 619df02..40381fe 100644 --- a/diana_services_api/mq_service_api.py +++ b/diana_services_api/mq_service_api.py @@ -40,13 +40,19 @@ class APIError(HTTPException): class MQServiceManager: def _validate_api_proxy_response(self, response: dict): if response['status_code'] == 200: - resp = json.loads(response['content']) - if isinstance(resp, dict): - return resp - # Reverse Geocode API returns a list; reformat that to a dict - if isinstance(resp, list): - return {**resp.pop(0), - **{"alternate_results": resp}} + try: + resp = json.loads(response['content']) + if isinstance(resp, dict): + return resp + # Reverse Geocode API returns a list; reformat that to a dict + if isinstance(resp, list): + return {**resp.pop(0), + **{"alternate_results": resp}} + except json.JSONDecodeError: + resp = response['content'] + # Wolfram Spoken API returns a string; reformat that to a dict + if isinstance(resp, str): + return {"answer": resp} code = response['status_code'] if response['status_code'] > 200 else 500 raise APIError(status_code=code, detail=response['content']) @@ -57,7 +63,6 @@ def query_api_proxy(self, service_name: str, query_params: dict, "neon_api_output", timeout) return self._validate_api_proxy_response(response) - def parse_ccl_script(self, script_text: str, metadata: dict = None, timeout: int = 30): response = send_mq_request("/neon_script_parser", diff --git a/diana_services_api/schema/api_requests.py b/diana_services_api/schema/api_requests.py index 17baf58..22b5d31 100644 --- a/diana_services_api/schema/api_requests.py +++ b/diana_services_api/schema/api_requests.py @@ -91,14 +91,25 @@ class WolframAlphaAPIRequest(BaseModel): lat: float lon: float query: str - - -class ParseScriptRequest(BaseModel): - script: str - - -class GetCouponsRequest(BaseModel): - pass + model_config = { + "json_schema_extra": { + "examples": [{ + "api": "spoken", + "lat": 47.6815, + "lon": -122.2087, + "query": "how far away is the moon" + }, { + "api": "short", + "lat": 47.6815, + "lon": -122.2087, + "query": "how far away is London" + }, { + "api": "full", + "lat": 47.6815, + "lon": -122.2087, + "query": "what is the derivative of sin(x)" + } + ]}} class SendEmailRequest(BaseModel): @@ -111,3 +122,11 @@ class SendEmailRequest(BaseModel): class UploadMetricRequest(BaseModel): metric_name: str metric_data: str + + +class ParseScriptRequest(BaseModel): + script: str + + +class GetCouponsRequest(BaseModel): + pass diff --git a/diana_services_api/schema/api_responses.py b/diana_services_api/schema/api_responses.py index fe68b05..f025148 100644 --- a/diana_services_api/schema/api_responses.py +++ b/diana_services_api/schema/api_responses.py @@ -1959,3 +1959,18 @@ class GeoAPIReverseResponse(BaseModel): "-122.2086379" ] }]}} + + +class WolframAlphaAPIResponse(BaseModel): + answer: str + model_config = { + "extra": "allow", + "json_schema_extra": { + "examples": [ + { + "answer": "The distance from Earth to the Moon at 4:29 P.M. Pacific Standard Time, Friday, January 12, 2024 is about 225192 miles" + }, { + "answer": "about 5378 miles" + }, { + "answer": "\n\n \n 1000\n input parameter not present in query\n \n" + }]}} From c9d8b253102116ae3f4485b08159754bad944e58 Mon Sep 17 00:00:00 2001 From: Daniel McKnight Date: Fri, 12 Jan 2024 17:50:26 -0800 Subject: [PATCH 04/17] Add remaining backend transactional endpoints --- diana_services_api/app.py | 16 ++ diana_services_api/mq_service_api.py | 49 +++- diana_services_api/schema/api_requests.py | 93 ++++++- diana_services_api/schema/api_responses.py | 278 +++++++++++++++++++++ 4 files changed, 422 insertions(+), 14 deletions(-) diff --git a/diana_services_api/app.py b/diana_services_api/app.py index 8258c57..d15dd8f 100644 --- a/diana_services_api/app.py +++ b/diana_services_api/app.py @@ -70,4 +70,20 @@ async def api_proxy_geolocation(query: GeoAPIReverseRequest) -> GeoAPIReverseRes async def api_proxy_wolframalpha(query: WolframAlphaAPIRequest) -> WolframAlphaAPIResponse: return mq_connector.query_api_proxy("wolfram_alpha", dict(query)) + @app.post("/email", dependencies=[Depends(jwt_bearer)]) + async def email_send(request: SendEmailRequest): + mq_connector.send_email(**dict(request)) + + @app.post("/metrics/upload", dependencies=[Depends(jwt_bearer)]) + async def upload_metric(metric: UploadMetricRequest): + mq_connector.upload_metric(**dict(metric)) + + @app.post("/ccl/parse", dependencies=[Depends(jwt_bearer)]) + async def parse_nct_script(script: ParseScriptRequest) -> ScriptParserResponse: + return mq_connector.parse_ccl_script(**dict(script)) + + @app.post("/coupons", dependencies=[Depends(jwt_bearer)]) + async def get_coupons() -> CouponsResponse: + return mq_connector.get_coupons() + return app diff --git a/diana_services_api/mq_service_api.py b/diana_services_api/mq_service_api.py index 40381fe..760f07d 100644 --- a/diana_services_api/mq_service_api.py +++ b/diana_services_api/mq_service_api.py @@ -25,6 +25,7 @@ # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. import json +from typing import Optional, Dict, Any from fastapi import HTTPException @@ -38,6 +39,9 @@ class APIError(HTTPException): class MQServiceManager: + def __init__(self): + self.mq_default_timeout = 10 + def _validate_api_proxy_response(self, response: dict): if response['status_code'] == 200: try: @@ -63,13 +67,44 @@ def query_api_proxy(self, service_name: str, query_params: dict, "neon_api_output", timeout) return self._validate_api_proxy_response(response) - def parse_ccl_script(self, script_text: str, metadata: dict = None, - timeout: int = 30): - response = send_mq_request("/neon_script_parser", - {"text": script_text, "metadata": metadata}, - "neon_script_parser_input", - "neon_script_parser_output", timeout) - return response + def send_email(self, recipient: str, subject: str, body: str, + attachments: Optional[Dict[str, str]]): + request_data = {"recipient": recipient, + "subject": subject, + "body": body, + "attachments": attachments} + response = send_mq_request("/neon_emails", request_data, + "neon_emails_input") + if not response.get("success"): + raise APIError(status_code=500, detail="Email failed to send") + + def upload_metric(self, metric_name: str, timestamp: str, + metric_data: Dict[str, Any]): + metric_data = {**{"name": metric_name, "timestamp": timestamp}, + **metric_data} + send_mq_request("/neon_metrics", metric_data, "neon_metrics_input", + expect_response=False) + + def parse_ccl_script(self, script: str, metadata: Dict[str, Any]): + try: + response = send_mq_request("/neon_script_parser", + {"text": script, "metadata": metadata}, + "neon_script_parser_input", + "neon_script_parser_output", + self.mq_default_timeout) + return {"ncs": response['parsed_file']} + except TimeoutError as e: + raise APIError(status_code=500, detail=repr(e)) + + def get_coupons(self): + try: + response = send_mq_request("/neon_coupons", {}, + "neon_coupons_input", + "neon_coupons_output", + self.mq_default_timeout) + return response + except TimeoutError as e: + raise APIError(status_code=500, detail=repr(e)) def get_stt(self, b64_audio: str, lang: str, timeout: int = 20): request_data = {"msg_type": "neon.get_stt", diff --git a/diana_services_api/schema/api_requests.py b/diana_services_api/schema/api_requests.py index 22b5d31..092049d 100644 --- a/diana_services_api/schema/api_requests.py +++ b/diana_services_api/schema/api_requests.py @@ -23,7 +23,7 @@ # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - +from time import time from typing import Optional from pydantic import BaseModel @@ -116,17 +116,96 @@ class SendEmailRequest(BaseModel): recipient: str subject: str body: str - attachments: Optional[str] = None + attachments: Optional[dict] = None + model_config = { + "json_schema_extra": { + "examples": [{ + "recipient": "developers@neon.ai", + "subject": "API test", + "body": "This is a test.\nGenerated from OpenAPI.", + "attachments": {"test.txt": "VGhpcyBpcyBhIHRlc3QgZmlsZQo="} + }]}} class UploadMetricRequest(BaseModel): metric_name: str - metric_data: str + timestamp: str + metric_data: dict = dict() + model_config = { + "json_schema_extra": { + "examples": [{ + "metric_name": "REST API Test", + "timestamp": str(time()), + "metric_data": {"test": True, "flag": "demo"}}]}} class ParseScriptRequest(BaseModel): script: str - - -class GetCouponsRequest(BaseModel): - pass + metadata: dict = dict() + model_config = { + "json_schema_extra": { + "examples": [{ + "script": """ +Script: Parser Test Script +Author: Daniel McKnight +Description: + Just an example description to go with + an example script. This will go in meta + +# Timeout goto line 18 +Timeout: 10, 18 +# Timeout exit +Timeout: 20 + +Synonym: "Test Script" + "Tester Script" + "Another Synonym" +Claps: 2 Two clap action + 3 3 clap action +Language: en-us, male + +Variable: no_val +Variable: with_val = "Test Value" + +# This is a comment line separating header from execution (kinda) +Neon speak: inlined speak +Neon speak: + Block speech start + ... + Block speech end +@pre-exec +Execute: hello world +voice_input(no_val) +IF no_val == with_val: + Goto: pre-exec +ELSE: + Reconvey: pre-exec + +If "word" IN "this phrase word is in": + Neon speak: "phrase" + +Reconvey: pre-exec, file_param +Name Reconvey: "Someone", "some text", "/path/to/file" + +Case {with_val}: + "Some value" + Neon speak: first + "some other value" + Neon speak: + second + +Case(no_val): + "no_val_1": + Execute: what time is it + +Python: 1*2 # TODO: syntax check + +LOOP check START +Set: new_val = no_val # This logs an error because it isn't declared +# TODO: The following should warn/error +dne = "test" +voice_input(new_val) +LOOP check END +Email: "Mail Title", "email body goes here. could be a variable name in most cases" +Run: script_name_here +Exit""", "metadata": {"test": True, "context": "demo"}}]}} diff --git a/diana_services_api/schema/api_responses.py b/diana_services_api/schema/api_responses.py index f025148..4b0ca73 100644 --- a/diana_services_api/schema/api_responses.py +++ b/diana_services_api/schema/api_responses.py @@ -1974,3 +1974,281 @@ class WolframAlphaAPIResponse(BaseModel): }, { "answer": "\n\n \n 1000\n input parameter not present in query\n \n" }]}} + + +class ScriptParserResponse(BaseModel): + ncs: str + model_config = { + "extra": "allow", + "json_schema_extra": { + "examples": [{ + "ncs": "gASV6xUAAAAAAABdlChdlCh9lCiMC2xpbmVfbnVtYmVylEsCjAR0ZXh0lIwSUGFyc2VyIFRlc3QgU2NyaXB0lIwGaW5kZW50lEsAjAdjb21tYW5klIwGc2NyaXB0lIwTcGFyZW50X2Nhc2VfaW5kZW50c5RdlIwEZGF0YZR9lIwFdGl0bGWUaAVzdX2UKGgDSwNoBIwPRGFuaWVsIE1jS25pZ2h0lGgGSwBoB4wGYXV0aG9ylGgJXZRoC32UaBBoD3N1fZQoaANLBGgEjACUaAZLAGgHjAtkZXNjcmlwdGlvbpRoCV2UdX2UKGgDSwVoBIwmSnVzdCBhbiBleGFtcGxlIGRlc2NyaXB0aW9uIHRvIGdvIHdpdGiUaAZLAWgHaBVoCV2UaAt9lCiMDmluX2Rlc2NyaXB0aW9ulIhoFWgYdXV9lChoA0sGaASMJ2FuIGV4YW1wbGUgc2NyaXB0LiBUaGlzIHdpbGwgZ28gaW4gbWV0YZRoBksBaAdoFWgJXZRoC32UKGgbiGgVaB11dX2UKGgDSwhoB05oBksAaAldlGgEjBYjIFRpbWVvdXQgZ290byBsaW5lIDE4lIwHY29tbWVudJSMFFRpbWVvdXQgZ290byBsaW5lIDE4lHV9lChoA0sJaASMBjEwLCAxOJRoBksAaAeMB3RpbWVvdXSUaAldlGgLfZQojAx0aW1lb3V0X3RpbWWUSwqMDnRpbWVvdXRfYWN0aW9ulIwCMTiUdXV9lChoA0sKaAdOaAZLAGgJXZRoBIwOIyBUaW1lb3V0IGV4aXSUaCOMDFRpbWVvdXQgZXhpdJR1fZQoaANLC2gEjAIyMJRoBksAaAdoJ2gJXZRoC32UKGgqSxRoK051dX2UKGgDSw1oBIwNIlRlc3QgU2NyaXB0IpRoBksAaAeMB3N5bm9ueW2UaAldlGgLfZSMCHN5bm9ueW1zlF2UjAtUZXN0IFNjcmlwdJRhc3V9lChoA0sOaASMDyJUZXN0ZXIgU2NyaXB0IpRoBksBaAdoN2gJXZRoC32UaDpdlIwNVGVzdGVyIFNjcmlwdJRhc3V9lChoA0sPaASMESJBbm90aGVyIFN5bm9ueW0ilGgGSwFoB2g3aAldlGgLfZRoOl2UjA9Bbm90aGVyIFN5bm9ueW2UYXN1fZQoaANLEGgEjBEyIFR3byBjbGFwIGFjdGlvbpRoBksAaAeMBWNsYXBzlGgJXZRoC32UKGhLjAEylIwGYWN0aW9ulIwPVHdvIGNsYXAgYWN0aW9ulHV1fZQoaANLEWgEjA8zIDMgY2xhcCBhY3Rpb26UaAZLAWgHaEtoCV2UaAt9lChoS4wBM5RoT4wNMyBjbGFwIGFjdGlvbpR1dX2UKGgDSxJoBIwLZW4tdXMsIG1hbGWUaAZLAGgHjAhsYW5ndWFnZZRoCV2UaAt9lCiMBmdlbmRlcpSMBG1hbGWUaFmMBWVuLXVzlHV1fZQoaANLFGgEjAZub192YWyUaAZLAGgHjAh2YXJpYWJsZZRoCV2UaAt9lCiMDXZhcmlhYmxlX25hbWWUaGCMDnZhcmlhYmxlX3ZhbHVllE6MDXZhcmlhYmxlX3R5cGWUjARsaXN0lIwSZGVjbGFyYXRpb25faW5kZW50lEsAjAtpbl92YXJpYWJsZZSIdXV9lChoA0sVaASMF3dpdGhfdmFsID0gIlRlc3QgVmFsdWUilGgGSwBoB2hhaAldlGgLfZQoaGSMCHdpdGhfdmFslGhljAwiVGVzdCBWYWx1ZSKUaGaMA3N0cpRoaEsAaGmIdXV9lChoA0sXaAdOaAZLAGgJXZRoBIxBIyBUaGlzIGlzIGEgY29tbWVudCBsaW5lIHNlcGFyYXRpbmcgaGVhZGVyIGZyb20gZXhlY3V0aW9uIChraW5kYSmUaCOMP1RoaXMgaXMgYSBjb21tZW50IGxpbmUgc2VwYXJhdGluZyBoZWFkZXIgZnJvbSBleGVjdXRpb24gKGtpbmRhKZR1fZQoaANLGGgEjA1pbmxpbmVkIHNwZWFrlGgGSwBoB4wFc3BlYWuUaAldlIwFdmFsaWSUiGgLfZQojARuYW1llIwETmVvbpSMBnBocmFzZZRodmhoSwCMCGluX3NwZWFrlIh1dX2UKGgDSxpoBIwSQmxvY2sgc3BlZWNoIHN0YXJ0lGgGSwFoB2h3aAt9lChoe2h8aH1ogGhoSwBofoh1aAldlHV9lChoA0sbaARoFGgGSwFoB2h3aAt9lChoe2h8aH1oFGhoSwBofoh1aAldlHV9lChoA0scaASMEEJsb2NrIHNwZWVjaCBlbmSUaAZLAWgHaHdoC32UKGh7aHxofWiHaGhLAGh+iHVoCV2UdX2UKGgDSx1oBIwJQHByZS1leGVjlGgGSwBoB4wDdGFnlGgJXZRoC32UjAVsYWJlbJSMCHByZS1leGVjlHN1fZQoaANLHmgEjAtoZWxsbyB3b3JsZJRoBksAaAeMB2V4ZWN1dGWUaAldlGh5iGgLfZRoB2iSc3V9lChoA0sfaASME3ZvaWNlX2lucHV0KG5vX3ZhbCmUaAZLAGgHjAt2b2ljZV9pbnB1dJRoCV2UaAt9lCiMDXZhcl90b19hc3NpZ26UjAZub192YWyUjAh2YXJfb3B0c5ROdXV9lChoA0sgaASMFklGIG5vX3ZhbCA9PSB3aXRoX3ZhbDqUaAZLAGgHjAJpZpRoCV2UaAt9lCiMBGxlZnSUjAZub192YWyUjAVyaWdodJSMCHdpdGhfdmFslIwKY29tcGFyYXRvcpSMAj09lHV1fZQoaANLIWgEjAhwcmUtZXhlY5RoBksBaAeMBGdvdG+UaAldlGgLfZSMC2Rlc3RpbmF0aW9ulGiqc3V9lChoA0siaARoFGgGSwBoB4wEZWxzZZRoCV2UdX2UKGgDSyNoBIwIcHJlLWV4ZWOUaAZLAWgHjAhyZWNvbnZleZRoCV2UaAt9lCiMDXJlY29udmV5X3RleHSUaLOMDXJlY29udmV5X2ZpbGWUaLN1dX2UKGgDSyVoBIwmSWYgIndvcmQiIElOICJ0aGlzIHBocmFzZSB3b3JkIGlzIGluIjqUaAZLAGgHaKBoCV2UaAt9lChoo4wGIndvcmQilGilXZSMFnRoaXMgcGhyYXNlIHdvcmQgaXMgaW6UYWinjAJJTpR1dX2UKGgDSyZoBIwIInBocmFzZSKUaAZLAWgHaHdoCV2UaHmIaAt9lChoe2h8aH1owmhoSwFofoh1dX2UKGgDSyhoBIwUcHJlLWV4ZWMsIGZpbGVfcGFyYW2UaAZLAGgHaLRoCV2UaAt9lChot4wIcHJlLWV4ZWOUaLiMCmZpbGVfcGFyYW2UdXV9lChoA0spaASMJyJTb21lb25lIiwgInNvbWUgdGV4dCIsICIvcGF0aC90by9maWxlIpRoBksAaAeMDW5hbWUgcmVjb252ZXmUaAldlGgLfZQoaHuMCSJTb21lb25lIpRot4wLInNvbWUgdGV4dCKUaLiMDS9wYXRoL3RvL2ZpbGWUdXV9lChoA0sraASMEENhc2Uge3dpdGhfdmFsfTqUaAZLAGgHjARjYXNllGgJXZRLAGFoC32UaGGMCnt3aXRoX3ZhbH2Uc3V9lChoA0ssaASMDCJTb21lIHZhbHVlIpRoBksBaAdo1WgJXZRLAGFoC32UjAdwaHJhc2VzlF2UjApTb21lIHZhbHVllGFzdX2UKGgDSy1oBIwFZmlyc3SUaAZLAmgHaHdoCV2USwBhaHmIaAt9lChoe2h8aH1o4WhoSwJofoh1dX2UKGgDSy5oBIwSInNvbWUgb3RoZXIgdmFsdWUilGgGSwFoB2jVaAldlEsAYWgLfZRo3V2UjBBzb21lIG90aGVyIHZhbHVllGFzdX2UKGgDSzBoBIwGc2Vjb25klGgGSwNoB2h3aAt9lChoe2h8aH1o62hoSwJofoh1aAldlEsAYXV9lChoA0syaASMDUNhc2Uobm9fdmFsKTqUaAZLAGgHaNVoCV2UKEsASwBlaAt9lGhhjAh7bm9fdmFsfZRzdX2UKGgDSzNoBGgUaAZLAWgHaNVoCV2UKEsASwBlaAt9lGjdXZRzdX2UKGgDSzRoBIwPd2hhdCB0aW1lIGlzIGl0lGgGSwJoB2iTaAldlChLAEsAZWh5iGgLfZRoB2j4c3V9lChoA0s2aASMAzEqMpRoI4wSVE9ETzogc3ludGF4IGNoZWNrlGgGSwBoB4wGcHl0aG9ulGgJXZRLAGF1fZQoaANLOGgEjBBMT09QIGNoZWNrIFNUQVJUlGgGSwBoB4wEbG9vcJRoCV2USwBhdX2UKGgDSzloBIwQbmV3X3ZhbCA9IG5vX3ZhbJRoI4wsVGhpcyBsb2dzIGFuIGVycm9yIGJlY2F1c2UgaXQgaXNuJ3QgZGVjbGFyZWSUaAZLAGgHjANzZXSUaAldlEsAYWgLfZQoaGGMB25ld192YWyUjAV2YWx1ZZSMBm5vX3ZhbJRoZmhwjAljbGVhbl92YXKUagoBAACMCXNldF9pbmRleJROaGhLAGhpiHV1fZQoaANLOmgHTmgGSwBoCV2UaASMJyMgVE9ETzogVGhlIGZvbGxvd2luZyBzaG91bGQgd2Fybi9lcnJvcpRoI4wlVE9ETzogVGhlIGZvbGxvd2luZyBzaG91bGQgd2Fybi9lcnJvcpR1fZQoaANLO2gEjAxkbmUgPSAidGVzdCKUaAZLAGgHTmgJXZR1fZQoaANLPGgEjBR2b2ljZV9pbnB1dChuZXdfdmFsKZRoBksAaAdomGgJXZR1fZQoaANLPWgEjA5MT09QIGNoZWNrIEVORJRoBksAaAdqAgEAAGgJXZR1fZQoaANLPmgEjEwiTWFpbCBUaXRsZSIsICJlbWFpbCBib2R5IGdvZXMgaGVyZS4gY291bGQgYmUgYSB2YXJpYWJsZSBuYW1lIGluIG1vc3QgY2FzZXMilGgGSwBoB4wFZW1haWyUaAldlGgLfZQojAdzdWJqZWN0lIwMIk1haWwgVGl0bGUilIwEYm9keZSMPiJlbWFpbCBib2R5IGdvZXMgaGVyZS4gY291bGQgYmUgYSB2YXJpYWJsZSBuYW1lIGluIG1vc3QgY2FzZXMilHV1fZQoaANLP2gEjBBzY3JpcHRfbmFtZV9oZXJllGgGSwBoB4wDcnVulGgJXZRoC32UjA1zY3JpcHRfdG9fcnVulGomAQAAc3V9lChoA0tAaASMBEV4aXSUaAZLAGgHjARleGl0lGgJXZR1ZX2UKGh7aHxoWWheaFxoXYwNb3ZlcnJpZGVfdXNlcpSIdX2UKGhgTmhuTnV9lIwFY2hlY2uUfZQojAVzdGFydJRLOIwDZW5klEs9dXN9lGiQSx1zSxROXZQoaDxoQmhIZX2UKGhOaFBoVWhWdX2UKIwIY3ZlcnNpb26UjAUwLjUuMJSMCGNvbXBpbGVklEpc6qFljAhjb21waWxlcpSMFU5lb24gQUkgU2NyaXB0IFBhcnNlcpRoDWgFaBBoD2gVjE8KSnVzdCBhbiBleGFtcGxlIGRlc2NyaXB0aW9uIHRvIGdvIHdpdGgKYW4gZXhhbXBsZSBzY3JpcHQuIFRoaXMgd2lsbCBnbyBpbiBtZXRhlIwIcmF3X2ZpbGWUWEAFAAAKU2NyaXB0OiBQYXJzZXIgVGVzdCBTY3JpcHQKQXV0aG9yOiBEYW5pZWwgTWNLbmlnaHQKRGVzY3JpcHRpb246CiAgICBKdXN0IGFuIGV4YW1wbGUgZGVzY3JpcHRpb24gdG8gZ28gd2l0aAogICAgYW4gZXhhbXBsZSBzY3JpcHQuIFRoaXMgd2lsbCBnbyBpbiBtZXRhCgojIFRpbWVvdXQgZ290byBsaW5lIDE4ClRpbWVvdXQ6IDEwLCAxOAojIFRpbWVvdXQgZXhpdApUaW1lb3V0OiAyMAoKU3lub255bTogIlRlc3QgU2NyaXB0IgogICAgIlRlc3RlciBTY3JpcHQiCiAgICAiQW5vdGhlciBTeW5vbnltIgpDbGFwczogMiBUd28gY2xhcCBhY3Rpb24KICAgIDMgMyBjbGFwIGFjdGlvbgpMYW5ndWFnZTogZW4tdXMsIG1hbGUKClZhcmlhYmxlOiBub192YWwKVmFyaWFibGU6IHdpdGhfdmFsID0gIlRlc3QgVmFsdWUiCgojIFRoaXMgaXMgYSBjb21tZW50IGxpbmUgc2VwYXJhdGluZyBoZWFkZXIgZnJvbSBleGVjdXRpb24gKGtpbmRhKQpOZW9uIHNwZWFrOiBpbmxpbmVkIHNwZWFrCk5lb24gc3BlYWs6CiAgICBCbG9jayBzcGVlY2ggc3RhcnQKICAgIC4uLgogICAgQmxvY2sgc3BlZWNoIGVuZApAcHJlLWV4ZWMKRXhlY3V0ZTogaGVsbG8gd29ybGQKdm9pY2VfaW5wdXQobm9fdmFsKQpJRiBub192YWwgPT0gd2l0aF92YWw6CiAgICBHb3RvOiBwcmUtZXhlYwpFTFNFOgogICAgUmVjb252ZXk6IHByZS1leGVjCgpJZiAid29yZCIgSU4gInRoaXMgcGhyYXNlIHdvcmQgaXMgaW4iOgogICAgTmVvbiBzcGVhazogInBocmFzZSIKClJlY29udmV5OiBwcmUtZXhlYywgZmlsZV9wYXJhbQpOYW1lIFJlY29udmV5OiAiU29tZW9uZSIsICJzb21lIHRleHQiLCAiL3BhdGgvdG8vZmlsZSIKCkNhc2Uge3dpdGhfdmFsfToKICAgICJTb21lIHZhbHVlIgogICAgICAgIE5lb24gc3BlYWs6IGZpcnN0CiAgICAic29tZSBvdGhlciB2YWx1ZSIKICAgICAgICBOZW9uIHNwZWFrOgogICAgICAgICAgICBzZWNvbmQKCkNhc2Uobm9fdmFsKToKICAgICJub192YWxfMSI6CiAgICAgICAgRXhlY3V0ZTogd2hhdCB0aW1lIGlzIGl0CgpQeXRob246IDEqMiAgIyBUT0RPOiBzeW50YXggY2hlY2sKCkxPT1AgY2hlY2sgU1RBUlQKU2V0OiBuZXdfdmFsID0gbm9fdmFsICAjIFRoaXMgbG9ncyBhbiBlcnJvciBiZWNhdXNlIGl0IGlzbid0IGRlY2xhcmVkCiMgVE9ETzogVGhlIGZvbGxvd2luZyBzaG91bGQgd2Fybi9lcnJvcgpkbmUgPSAidGVzdCIKdm9pY2VfaW5wdXQobmV3X3ZhbCkKTE9PUCBjaGVjayBFTkQKRW1haWw6ICJNYWlsIFRpdGxlIiwgImVtYWlsIGJvZHkgZ29lcyBoZXJlLiBjb3VsZCBiZSBhIHZhcmlhYmxlIG5hbWUgaW4gbW9zdCBjYXNlcyIKUnVuOiBzY3JpcHRfbmFtZV9oZXJlCkV4aXSUdX2UKGhgaGdobmhwdWUu" + }]}} + + +class CouponsResponse(BaseModel): + success: bool + brands: List[str] + coupons: List[str] + + model_config = { + "extra": "allow", + "json_schema_extra": { + "examples": [{ + "success": True, + "brands": [ + "amazon", + "blue apron", + "home depot", + "old navy", + "bed bath and beyond", + "sears", + "dominos", + "budget car rental", + "orbitz", + "target", + "kohls", + "nordstrom", + "amazon prime now", + "apple", + "google", + "coca cola", + "microsoft", + "mycroft", + "samsung", + "ebikes", + "kroll maps", + "brand", + "harrypotter", + "conversation processing intelligence", + "value added websites", + "neon", + "steve jones", + "alpha", + "beta", + "gamma", + "december", + "strata", + "intelligent", + "flower", + "argon", + "steve", + "elon 5000", + "theta", + "pool", + "josh", + "test", + "grass", + "testing", + "demo", + "mack", + "june", + "door dash", + "newegg", + "amtrak", + "toms", + "graco", + "otterbox", + "auto zone", + "uber", + "papa johns", + "bed bath and beyond", + "target", + "macys", + "best buy", + "dominos", + "office depot", + "kohls", + "bath and body works", + "best buy", + "budget rental car", + "carters", + "dicks sporting goods", + "door dash", + "enterprise rental car", + "famous footware", + "fashion nova", + "home depot", + "hotels.com", + "jcpenney", + "michaels", + "old navy", + "oriental trading company", + "pizza hut", + "post mates", + "sephora", + "shutterfly", + "southwest airlines", + "uber eats", + "ulta", + "victorias secret", + "spirit airlines", + "rock auto", + "vistaprint", + "panera bread", + "ebay", + "walgreens", + "revolve", + "priceline", + "hotels.com", + "1800flowers", + "airbnb", + "staples", + "jet.com", + "dell.com", + "jomashop", + "rakuten", + "walmart", + "groupon", + "barnes and noble", + "new york times", + "old navy", + "sprint", + "delta", + "alaska air", + "ford", + "mcdonalds", + "taco bell", + "red lobster", + "olive garden", + "olive garden", + "applebees", + "starbucks", + "target", + "bed bath and beyond", + "ticketmaster", + "airbnb", + "dominos", + "papa johns", + "door dash", + "ashley home store", + "august" + ], + "coupons": [ + "\"1800flowers\",\"20% Off Flowers And Gifts\",\"SAVETWENTY\"", + "\"airbnb\",\"Save 10% on your AirBnB booking\",\"SAVE10\"", + "\"airbnb\",\"$40 off your booking with Airbnb\",\"dsenter10\"", + "\"alaska air\",\"5% Off Flights For Insider Members\",\"EC6208\"", + "\"alpha\",\"Save 10% off with Alpha Brand!\",\"ALF10\"", + "\"amazon\",\"NEW CUSTOMERS! $10 OFF YOUR FIRST PRIME NOW ORDER.\",\"10PRIMENOW\"", + "\"amazon prime now\",\"Up to $20 Off Your First Orders Through Prime Now Or Whole Foods Market\",\"20PRIMEDAY\"", + "\"amtrak\",\"Buy One Ticket, Get One Free When You Share a Bedroom or Roomette\",\"V540\"", + "\"apple\",\"$5 Cash Back on $50 for Beats, iPods, and Accessories\",\"ONLINE\"", + "\"applebees\",\"$5 Off $25+ Your First Online Order\",\"5OFF25\"", + "\"argon\",\"Save 10% on Argon\",\"Argon10off\"", + "\"ashley home store\",\"Up to 70% Off + Extra 10% Off + 12 Months Special Financing\",\"POPUP19\"", + "\"auto zone\",\"10% on Auto Zone Orders Online or In store\",\"NGZone\"", + "\"barnes and noble\",\"25% Off All Eligible NOOK Book Bash Items With Coupon Code\",\"NOOKBASH25\"", + "\"bath and body works\",\"20% Off With Promo Code online\",\"TWYSURT\"", + "\"bed bath and beyond\",\"20% Off One Item In-Store\",\"20OFFBBB\"", + "\"bed bath and beyond\",\"20% Off online orders\",\"20OFF\"", + "\"bed bath and beyond\",\"Save 20% off when you sign up for emails.\",\"None needed\"", + "\"best buy\",\"Save 20% on regular-priced appliances with promo code\",\"SAVEONSMALLSNOW\"", + "\"best buy\",\"20% Off one regular priced item\",\"APPLY20RMNNOW\"", + "\"beta\",\"Save 20% off BETA and free shipping\",\"BETA20\"", + "\"blue apron\",\"$30 OFF YOUR FIRST DELIVERY!\",\"BA17B25\"", + "\"brand\",\"A 1-2-3 Punch of savings\",\"Brand123\"", + "\"budget car rental\",\"Up to $25 Off Base Rate on your Car Rental with Minimum Spend\",\"MUWZ092\"", + "\"budget rental car\",\"$40 Off Intermediate Or Larger Vehicle Rent\",\"MUGZ025\"", + "\"carters\",\"Extra 20% Off Your $50+ In-Store Purchase\",\"CART20\"", + "\"coca cola\",\"GET REWARDED WHEN YOU BUY COKE PRODUCTS\",\"REWARDS\"", + "\"conversation processing intelligence\",\"Get free conversation transcription\",\"CPI180822\"", + "\"december\",\"Get 10% off in December\",\"DECEMBER10\"", + "\"dell.com\",\"15% off site wide\",\"SAVE15\"", + "\"delta\",\"Up to $250 Off Summer Vacation Bookings To The Caribbean + Earn 2,500 Extra Bonus Miles Per Person\",\"DVSUMMERA\"", + "\"demo\",\"Save 10% off demo\",\"Demo10\"", + "\"dicks sporting goods\",\"Extra 10% Off Next Purchase With Dick's Sporting Goods Email Sign Up\",\"No Code needed\"", + "\"dominos\",\"Carryout Large 3-Topping Pizza for $7.99\",\"9174\"", + "\"dominos\",\"30% off Large Traditional & Premium Pizzas, Pick up or Delivered\",\"355852\"", + "\"dominos\",\"40% off your order of regular priced pizza\",\"222233\"", + "\"door dash\",\"$5 off $10 on Pickup Orders\",\"PICKUPTIME\"", + "\"door dash\",\"$5 Off $15\",\"NOVDASH18\"", + "\"door dash\",\"$15 off your order\",\"FSt4vk\"", + "\"ebay\",\"$5 Off Your Order For New Users\",\"WELCOME5\"", + "\"ebikes\",\"10% off ebike kit #1\",\"EB01\"", + "\"elon 5000\",\"Brightest mind in the Northwest\",\"XYZ\"", + "\"enterprise rental car\",\"10 Off when you book a luxury car\",\"10Off\"", + "\"famous footware\",\"$10 Off Your Orders of $50+\",\"ENTR2018\"", + "\"fashion nova\",\"30% Off Your Fashion Nova Purchase + Free Shipping Over $75\",\"NOVABABE5-56NS46\"", + "\"flower\",\"Save 10% off all flowers!\",\"Flower10\"", + "\"ford\",\"10% Off Sitewide\",\"FORDSUMMER14\"", + "\"gamma\",\"save on all of your GAMMA needs!\",\"GAMMA30\"", + "\"google\",\"$15 off your purchase WITH GOOGLE EXPRESS\",\"KGUCT33MF\"", + "\"graco\",\"20% Off Sitewide (Excluding 4Ever)\",\"SUMMEROFGRACO\"", + "\"grass\",\"Save 10% off Grass\",\"Grass10\"", + "\"groupon\",\"SAVE10\",\"$10 off your order\"", + "\"guy's ebikes\",\"20% off\",\"EB1\"", + "\"harrypotter\",\"Get a free wizard!\",\"HarryPotterWizard\"", + "\"home depot\",\"Additional 10% Off Cutlery Items And Accessories\",\"CHOPUPSAVINGS\"", + "\"home depot\",\"$5 Off Coupon With Home Depot Email Signup\",\"No Coupon Code\"", + "\"hotels.com\",\"Extra 10% Off Select Hotels\",\"RC10\"", + "\"hotels.com\",\"Extra 10% Off Select Hotels\",\"WEDDING10\"", + "\"intelligent\",\"SAve 10% off intelligent devices\",\"Intelligent10\"", + "\"jcpenney\",\"20% In Store and Online\",\"GIFTDAD\"", + "\"jet.com\",\"30% off all Grocery Pup items\",\"GROCERYPUP\"", + "\"jomashop\",\"$10 off orders of $150\",\"AD10\"", + "\"josh\",\"Josh saves 10%\",\"Josh10\"", + "\"june\",\"save 10% on June! Wow!\",\"JUNE10\"", + "\"kohls\",\"Save an Extra 20% Off\",\"FIREWORK\"", + "\"kohls\",\"15% Off $100 or More + Free Shipping\",\"CATCH15OFF\"", + "\"kroll maps\",\"20% off all European Maps!!!\",\"KR01\"", + "\"mack\",\"Save 10% off all MAck products\",\"Mack10\"", + "\"macys\",\"30% Off Lauren Ralph Lauren\",\"FRIEND\"", + "\"mcdonalds\",\"McDonald's Offers, Codes, In-store Coupons, And More\",\"Sign up in App\"", + "\"michaels\",\"50% Off One Regular-Priced Item With Michaels Coupon\",\"50HALFBDAY\"", + "\"microsoft\",\"10% off + Free shipping for students and parents\",\"10% OFF\"", + "\"mycroft\",\"KICKSTARTER - Pledge $299 or more 3-Pack of Mark II devices\",\"MARKII -\"", + "\"neon\",\"Save 20% when you buy a NeonX 10 inch audio pc.\",\"NeonXSave20\"", + "\"new york times\",\"15% Off Orders Over $50\",\"MOM15\"", + "\"newegg\",\"5% Off $50+ on Select CPUs, Input Devices & More\",\"CORNSAVE519\"", + "\"nordstrom\",\"Free 21-Piece Gift With Your $75 Beauty Or Fragrance Purchase\",\"TEAL\"", + "\"office depot\",\"20% Off Your Qualifying Regular Priced Purchase\",\"DMXP6\"", + "\"old navy\",\"OHYES\",\"ohyes\"", + "\"old navy\",\"20% Off Sitewide\",\"SWEET\"", + "\"old navy\",\"20% Off Your Purchase With Old Navy Email Sign-up\",\"No Code needed\"", + "\"olive garden\",\"Free Appetizer Or Dessert With Olive Garden Email Signup\",\"No Code needed\"", + "\"olive garden\",\"$2 Off 2 Lunches\",\"2OFF2L\"", + "\"orbitz\",\"15% Off Select Hotels\",\"HEATWAVE\"", + "\"oriental trading company\",\"Up to $40 Off + Free Shipping on $49\",\"6SAVENOW\"", + "\"otterbox\",\"10% off\",\"ULTIMATE10\"", + "\"panera bread\",\"50% Off Orders $25+ For Rapid Pick-Up Order\",\"MMDRF\"", + "\"papa johns\",\"30% Off Regular Menu-Priced Orders\",\"GET30\"", + "\"papa johns\",\"30% Off Regular Menu-Priced Orders with Promo Code!\",\"GET30\"", + "\"pizza hut\",\"BF9VY9VE4XE2\",\"BF9VY9VE4XE2\"", + "\"pool\",\"save on your pool!\",\"pool10\"", + "\"post mates\",\"$100 in delivery credits\",\"FOOD4YOU\"", + "\"priceline\",\"8% Off Select Hotels\",\"RMNJUN8\"", + "\"rakuten\",\"20% Clothing, Shoes and Accessories\",\"APPAREL20\"", + "\"red lobster\",\"10% Off Any To Go Order\",\"LOBSTER75\"", + "\"revolve\",\"20% Off Sitewide\",\"REVOLVE4AU\"", + "\"rock auto\",\"5% off your order\",\"10703653554843330\"", + "\"samsung\",\"$100 Off Samsung POWERbot Robot Vacuum + Additional $50 Off + Free Shipping\",\"PLXGUA9Z7\"", + "\"sears\",\"Extra $35 Off $300+ on Home Appliances, Lawn & Garden, Tools, Mattresses & Sporting Goods\",\"SEARS35OFF300\"", + "\"sephora\",\"FREE gift with select purchase online only\",\"PICKYOURS\"", + "\"shutterfly\",\"30% Off Sitewide\",\"30SAVINGS\"", + "\"southwest airlines\",\"Up to 35% Off Base Rate For Hertz Rentals + Up to 2400 Rapid Rewards Points\",\"159062\"", + "\"spirit airlines\",\"$50 off bookings\",\"CD50\"", + "\"sprint\",\"50% Off When You Upgrade\",\"No Code needed\"", + "\"staples\",\"$15 Off Orders of $100+\",\"67914\"", + "\"starbucks\",\"$5 Gift With Your Order\",\"wjmnm\"", + "\"steve\",\"Test of Steve\",\"test\"", + "\"steve jones\",\"Free Call From Steve\",\"steve\"", + "\"strata\",\"save 10% off strata\",\"STRATA10\"", + "\"taco bell\",\"10% Off Online Order\",\"Save 10% when you order online\"", + "\"target\",\"$5 Off $50 Select Items + Free Shipping on Qualifying Purchases\",\"90209\"", + "\"target\",\"$5 Off $50 Select Items at Target + Free Shipping\",\"No Code needed\"", + "\"target\",\"$5 Off $50 Select Items at Target + Free Shipping\",\"FIVEOFF\"", + "\"test\",\"save 10% off test\",\"Test10\"", + "\"testing\",\"Save 10% off\",\"Test10\"", + "\"theta\",\"saave 10% off Theta\",\"theta10\"", + "\"ticketmaster\",\"Save 50% when you buy two tickets\",\"TMN241\"", + "\"toms\",\"$10 off any order and free shipping\",\"CHANGE\"", + "\"uber\",\"$5 off each of your first 3 trips\",\"NEWRIDER15\"", + "\"uber eats\",\"40% Off First Order\",\"SAVE40\"", + "\"ulta\",\"401534\",\"401534\"", + "\"value added websites\",\"100% off all new websites!\",\"VAW01\"", + "\"victorias secret\",\"FREE shipping on orders over $50\",\"SHIP50\"", + "\"vistaprint\",\"Up to 50% Off Everything Only at Vistaprint!\",\"SALE50\"", + "\"walgreens\",\"50% Off Prints ...\",\"COOLPIX\"", + "\"walmart\",\"$10 Off Orders $50+ at Walmart Grocery\",\"LA9ARAAC\"" + ]}]}} From 16d60676b6503a4a69f17a757c45a89587d2a577 Mon Sep 17 00:00:00 2001 From: Daniel McKnight Date: Fri, 12 Jan 2024 18:47:19 -0800 Subject: [PATCH 05/17] Refactor into modules --- diana_services_api/app.py | 89 -------------------- diana_services_api/app/__init__.py | 41 +++++++++ diana_services_api/{ => app}/__main__.py | 1 + diana_services_api/app/dependencies.py | 33 ++++++++ diana_services_api/app/routers/__init__.py | 25 ++++++ diana_services_api/app/routers/api_proxy.py | 67 +++++++++++++++ diana_services_api/app/routers/auth.py | 37 ++++++++ diana_services_api/app/routers/mq_backend.py | 53 ++++++++++++ diana_services_api/auth/client_manager.py | 19 +++-- diana_services_api/mq_service_api.py | 4 +- tests/test_auth.py | 2 +- 11 files changed, 270 insertions(+), 101 deletions(-) delete mode 100644 diana_services_api/app.py create mode 100644 diana_services_api/app/__init__.py rename diana_services_api/{ => app}/__main__.py (98%) create mode 100644 diana_services_api/app/dependencies.py create mode 100644 diana_services_api/app/routers/__init__.py create mode 100644 diana_services_api/app/routers/api_proxy.py create mode 100644 diana_services_api/app/routers/auth.py create mode 100644 diana_services_api/app/routers/mq_backend.py diff --git a/diana_services_api/app.py b/diana_services_api/app.py deleted file mode 100644 index d15dd8f..0000000 --- a/diana_services_api/app.py +++ /dev/null @@ -1,89 +0,0 @@ -# NEON AI (TM) SOFTWARE, Software Development Kit & Application Development System -# All trademark and other rights reserved by their respective owners -# Copyright 2008-2021 Neongecko.com Inc. -# BSD-3 -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are met: -# 1. Redistributions of source code must retain the above copyright notice, -# this list of conditions and the following disclaimer. -# 2. Redistributions in binary form must reproduce the above copyright notice, -# this list of conditions and the following disclaimer in the documentation -# and/or other materials provided with the distribution. -# 3. Neither the name of the copyright holder nor the names of its -# contributors may be used to endorse or promote products derived from this -# software without specific prior written permission. -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, -# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, -# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, -# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, -# OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF -# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING -# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -from fastapi import FastAPI, Depends - -from diana_services_api.schema.auth_requests import * -from diana_services_api.schema.api_requests import * -from diana_services_api.schema.api_responses import * -from diana_services_api.mq_service_api import MQServiceManager -from diana_services_api.auth.client_manager import ClientManager, JWTBearer - - -def create_app(): - mq_connector = MQServiceManager() - client_manager = ClientManager() - jwt_bearer = JWTBearer(client_manager) - app = FastAPI() - - @app.post("/auth/login") - async def check_login(request: AuthenticationRequest) -> AuthenticationResponse: - return client_manager.check_auth_request(**dict(request)) - - @app.post("/proxy/weather", dependencies=[Depends(jwt_bearer)]) - async def api_proxy_weather(query: WeatherAPIRequest) -> WeatherAPIOnecallResponse: - return mq_connector.query_api_proxy("open_weather_map", dict(query)) - - @app.post("/proxy/stock/symbol", dependencies=[Depends(jwt_bearer)]) - async def api_proxy_stock_symbol(query: StockAPISymbolRequest) -> StockAPISearchResponse: - return mq_connector.query_api_proxy("alpha_vantage", - {**dict(query), - **{"api": "symbol"}}) - - @app.post("/proxy/stock/quote", dependencies=[Depends(jwt_bearer)]) - async def api_proxy_stock_quote(query: StockAPIQuoteRequest) -> StockAPIQuoteResponse: - return mq_connector.query_api_proxy("alpha_vantage", - {**dict(query), **{"api": "quote"}}) - - @app.post("/proxy/geolocation/geocode", dependencies=[Depends(jwt_bearer)]) - async def api_proxy_geolocation(query: GeoAPIRequest) -> GeoAPIGeocodeResponse: - return mq_connector.query_api_proxy("map_maker", dict(query)) - - @app.post("/proxy/geolocation/reverse", dependencies=[Depends(jwt_bearer)]) - async def api_proxy_geolocation(query: GeoAPIReverseRequest) -> GeoAPIReverseResponse: - return mq_connector.query_api_proxy("map_maker", dict(query)) - - @app.post("/proxy/wolframalpha", dependencies=[Depends(jwt_bearer)]) - async def api_proxy_wolframalpha(query: WolframAlphaAPIRequest) -> WolframAlphaAPIResponse: - return mq_connector.query_api_proxy("wolfram_alpha", dict(query)) - - @app.post("/email", dependencies=[Depends(jwt_bearer)]) - async def email_send(request: SendEmailRequest): - mq_connector.send_email(**dict(request)) - - @app.post("/metrics/upload", dependencies=[Depends(jwt_bearer)]) - async def upload_metric(metric: UploadMetricRequest): - mq_connector.upload_metric(**dict(metric)) - - @app.post("/ccl/parse", dependencies=[Depends(jwt_bearer)]) - async def parse_nct_script(script: ParseScriptRequest) -> ScriptParserResponse: - return mq_connector.parse_ccl_script(**dict(script)) - - @app.post("/coupons", dependencies=[Depends(jwt_bearer)]) - async def get_coupons() -> CouponsResponse: - return mq_connector.get_coupons() - - return app diff --git a/diana_services_api/app/__init__.py b/diana_services_api/app/__init__.py new file mode 100644 index 0000000..44970f8 --- /dev/null +++ b/diana_services_api/app/__init__.py @@ -0,0 +1,41 @@ +# NEON AI (TM) SOFTWARE, Software Development Kit & Application Development System +# All trademark and other rights reserved by their respective owners +# Copyright 2008-2021 Neongecko.com Inc. +# BSD-3 +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from this +# software without specific prior written permission. +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +# OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +from fastapi import FastAPI + +from diana_services_api.app.dependencies import client_manager, jwt_bearer, mq_connector +from diana_services_api.app.routers.api_proxy import proxy_route +from diana_services_api.app.routers.mq_backend import mq_route +from diana_services_api.app.routers.auth import auth_route + + +def create_app(): + app = FastAPI() + app.include_router(auth_route) + app.include_router(proxy_route) + app.include_router(mq_route) + + return app diff --git a/diana_services_api/__main__.py b/diana_services_api/app/__main__.py similarity index 98% rename from diana_services_api/__main__.py rename to diana_services_api/app/__main__.py index 951b1d8..ab95303 100644 --- a/diana_services_api/__main__.py +++ b/diana_services_api/app/__main__.py @@ -31,6 +31,7 @@ def main(): app = create_app() + # TODO: host, port from config uvicorn.run(app, host="0.0.0.0", port=8080) diff --git a/diana_services_api/app/dependencies.py b/diana_services_api/app/dependencies.py new file mode 100644 index 0000000..926a177 --- /dev/null +++ b/diana_services_api/app/dependencies.py @@ -0,0 +1,33 @@ +# NEON AI (TM) SOFTWARE, Software Development Kit & Application Development System +# All trademark and other rights reserved by their respective owners +# Copyright 2008-2021 Neongecko.com Inc. +# BSD-3 +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from this +# software without specific prior written permission. +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +# OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +from diana_services_api.mq_service_api import MQServiceManager +from diana_services_api.auth.client_manager import ClientManager, UserTokenAuth + +config = dict() # TODO +mq_connector = MQServiceManager(config) +client_manager = ClientManager(config) +jwt_bearer = UserTokenAuth(client_manager) diff --git a/diana_services_api/app/routers/__init__.py b/diana_services_api/app/routers/__init__.py new file mode 100644 index 0000000..d782cbb --- /dev/null +++ b/diana_services_api/app/routers/__init__.py @@ -0,0 +1,25 @@ +# NEON AI (TM) SOFTWARE, Software Development Kit & Application Development System +# All trademark and other rights reserved by their respective owners +# Copyright 2008-2021 Neongecko.com Inc. +# BSD-3 +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from this +# software without specific prior written permission. +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +# OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/diana_services_api/app/routers/api_proxy.py b/diana_services_api/app/routers/api_proxy.py new file mode 100644 index 0000000..d263338 --- /dev/null +++ b/diana_services_api/app/routers/api_proxy.py @@ -0,0 +1,67 @@ +# NEON AI (TM) SOFTWARE, Software Development Kit & Application Development System +# All trademark and other rights reserved by their respective owners +# Copyright 2008-2021 Neongecko.com Inc. +# BSD-3 +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from this +# software without specific prior written permission. +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +# OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +from fastapi import APIRouter, Depends +from diana_services_api.schema.api_requests import * +from diana_services_api.schema.api_responses import * +from diana_services_api.app.dependencies import jwt_bearer, mq_connector + + +proxy_route = APIRouter(prefix="/proxy", tags=["backend"], + dependencies=[Depends(jwt_bearer)]) + + +@proxy_route.post("/weather") +async def api_proxy_weather(query: WeatherAPIRequest) -> WeatherAPIOnecallResponse: + return mq_connector.query_api_proxy("open_weather_map", dict(query)) + + +@proxy_route.post("/stock/symbol") +async def api_proxy_stock_symbol(query: StockAPISymbolRequest) -> StockAPISearchResponse: + return mq_connector.query_api_proxy("alpha_vantage", + {**dict(query), + **{"api": "symbol"}}) + + +@proxy_route.post("/stock/quote") +async def api_proxy_stock_quote(query: StockAPIQuoteRequest) -> StockAPIQuoteResponse: + return mq_connector.query_api_proxy("alpha_vantage", + {**dict(query), **{"api": "quote"}}) + + +@proxy_route.post("/geolocation/geocode") +async def api_proxy_geolocation(query: GeoAPIRequest) -> GeoAPIGeocodeResponse: + return mq_connector.query_api_proxy("map_maker", dict(query)) + + +@proxy_route.post("/geolocation/reverse") +async def api_proxy_geolocation(query: GeoAPIReverseRequest) -> GeoAPIReverseResponse: + return mq_connector.query_api_proxy("map_maker", dict(query)) + + +@proxy_route.post("/wolframalpha") +async def api_proxy_wolframalpha(query: WolframAlphaAPIRequest) -> WolframAlphaAPIResponse: + return mq_connector.query_api_proxy("wolfram_alpha", dict(query)) \ No newline at end of file diff --git a/diana_services_api/app/routers/auth.py b/diana_services_api/app/routers/auth.py new file mode 100644 index 0000000..6d1b8f1 --- /dev/null +++ b/diana_services_api/app/routers/auth.py @@ -0,0 +1,37 @@ +# NEON AI (TM) SOFTWARE, Software Development Kit & Application Development System +# All trademark and other rights reserved by their respective owners +# Copyright 2008-2021 Neongecko.com Inc. +# BSD-3 +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from this +# software without specific prior written permission. +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +# OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +from fastapi import APIRouter + +from diana_services_api.app.dependencies import client_manager +from diana_services_api.schema.auth_requests import * + +auth_route = APIRouter(prefix="/auth", tags=["authentication"]) + + +@auth_route.post("/login") +async def check_login(request: AuthenticationRequest) -> AuthenticationResponse: + return client_manager.check_auth_request(**dict(request)) diff --git a/diana_services_api/app/routers/mq_backend.py b/diana_services_api/app/routers/mq_backend.py new file mode 100644 index 0000000..1d11546 --- /dev/null +++ b/diana_services_api/app/routers/mq_backend.py @@ -0,0 +1,53 @@ +# NEON AI (TM) SOFTWARE, Software Development Kit & Application Development System +# All trademark and other rights reserved by their respective owners +# Copyright 2008-2021 Neongecko.com Inc. +# BSD-3 +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from this +# software without specific prior written permission. +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +# OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +from fastapi import APIRouter, Depends +from diana_services_api.schema.api_requests import * +from diana_services_api.schema.api_responses import * +from diana_services_api.app.dependencies import jwt_bearer, mq_connector + + +mq_route = APIRouter(tags=["backend"], dependencies=[Depends(jwt_bearer)]) + + +@mq_route.post("/email", dependencies=[Depends(jwt_bearer)]) +async def email_send(request: SendEmailRequest): + mq_connector.send_email(**dict(request)) + + +@mq_route.post("/metrics/upload", dependencies=[Depends(jwt_bearer)]) +async def upload_metric(metric: UploadMetricRequest): + mq_connector.upload_metric(**dict(metric)) + + +@mq_route.post("/ccl/parse", dependencies=[Depends(jwt_bearer)]) +async def parse_nct_script(script: ParseScriptRequest) -> ScriptParserResponse: + return mq_connector.parse_ccl_script(**dict(script)) + + +@mq_route.post("/coupons", dependencies=[Depends(jwt_bearer)]) +async def get_coupons() -> CouponsResponse: + return mq_connector.get_coupons() diff --git a/diana_services_api/auth/client_manager.py b/diana_services_api/auth/client_manager.py index 626f348..ded3f0c 100644 --- a/diana_services_api/auth/client_manager.py +++ b/diana_services_api/auth/client_manager.py @@ -28,20 +28,21 @@ from time import time from typing import Dict, Optional - from fastapi import Request, HTTPException from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials from jwt import DecodeError class ClientManager: - def __init__(self): + def __init__(self, config: dict): self.authorized_clients: Dict[str, dict] = dict() - # TODO: secrets and expirations from config - self._access_token_lifetime = 3600 * 24 # 1 day - self._refresh_token_lifetime = 3600 * 24 * 7 # 1 week - self._access_secret = 'a800445648142061fc238d1f84e96200da87f4f9f784108ac90db8b4391b117b' - self._refresh_secret = '833d369ac73d883123743a44b4a7fe21203cffc956f4c8a99be6e71aafa8e1aa' + self._access_token_lifetime = config.get("access_token_ttl", 3600 * 24) + self._refresh_token_lifetime = config.get("refresh_token_ttl", + 3600 * 24 * 7) + self._access_secret = config.get("access_token_secret", + 'a800445648142061fc238d1f84e96200da87f4f9f784108ac90db8b4391b117b') + self._refresh_secret = config.get("refresh_token_secret", + '833d369ac73d883123743a44b4a7fe21203cffc956f4c8a99be6e71aafa8e1aa') self._jwt_algo = "HS256" def check_auth_request(self, client_id: str, username: str, @@ -82,14 +83,14 @@ def validate_auth(self, token: str) -> bool: return False -class JWTBearer(HTTPBearer): +class UserTokenAuth(HTTPBearer): def __init__(self, client_manager: ClientManager): HTTPBearer.__init__(self) self.client_manager = client_manager async def __call__(self, request: Request): credentials: HTTPAuthorizationCredentials = \ - await super(JWTBearer, self).__call__(request) + await HTTPBearer.__call__(self, request) if credentials: if not credentials.scheme == "Bearer": raise HTTPException(status_code=403, diff --git a/diana_services_api/mq_service_api.py b/diana_services_api/mq_service_api.py index 760f07d..84dbc20 100644 --- a/diana_services_api/mq_service_api.py +++ b/diana_services_api/mq_service_api.py @@ -39,8 +39,8 @@ class APIError(HTTPException): class MQServiceManager: - def __init__(self): - self.mq_default_timeout = 10 + def __init__(self, config: dict): + self.mq_default_timeout = config.get('mq_default_timeout', 10) def _validate_api_proxy_response(self, response: dict): if response['status_code'] == 200: diff --git a/tests/test_auth.py b/tests/test_auth.py index 823ccd6..f535005 100644 --- a/tests/test_auth.py +++ b/tests/test_auth.py @@ -30,7 +30,7 @@ class TestClientManager(unittest.TestCase): from diana_services_api.auth.client_manager import ClientManager - client_manager = ClientManager() + client_manager = ClientManager({}) def test_check_auth_request(self): client_1 = str(uuid4()) From 268f82c83cbdc31124be9e541dcbea9efb8b4e85 Mon Sep 17 00:00:00 2001 From: Daniel McKnight Date: Fri, 12 Jan 2024 18:55:56 -0800 Subject: [PATCH 06/17] Stub config handling Add site title and summary --- diana_services_api/app/__init__.py | 8 ++++++-- diana_services_api/app/__main__.py | 3 ++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/diana_services_api/app/__init__.py b/diana_services_api/app/__init__.py index 44970f8..5bb95ef 100644 --- a/diana_services_api/app/__init__.py +++ b/diana_services_api/app/__init__.py @@ -30,10 +30,14 @@ from diana_services_api.app.routers.api_proxy import proxy_route from diana_services_api.app.routers.mq_backend import mq_route from diana_services_api.app.routers.auth import auth_route +from diana_services_api.version import __version__ -def create_app(): - app = FastAPI() +def create_app(config: dict): + title = config.get('title') or "Diana Services API" + summary = config.get('summary') or "HTTP component of the Device Independent API for Neon Applications (DIANA)" + version = __version__ + app = FastAPI(title=title, summary=summary, version=version) app.include_router(auth_route) app.include_router(proxy_route) app.include_router(mq_route) diff --git a/diana_services_api/app/__main__.py b/diana_services_api/app/__main__.py index ab95303..98a1105 100644 --- a/diana_services_api/app/__main__.py +++ b/diana_services_api/app/__main__.py @@ -30,7 +30,8 @@ def main(): - app = create_app() + config = dict() + app = create_app(config) # TODO: host, port from config uvicorn.run(app, host="0.0.0.0", port=8080) From 5939be609c8c0506de0f520496eb3ae871886027 Mon Sep 17 00:00:00 2001 From: Daniel McKnight Date: Tue, 16 Jan 2024 16:53:55 -0800 Subject: [PATCH 07/17] Implement LLM API route --- diana_services_api/app/__init__.py | 2 + diana_services_api/app/routers/llm.py | 58 +++++++++++++++++++++++ diana_services_api/mq_service_api.py | 16 ++++++- diana_services_api/schema/llm_requests.py | 54 +++++++++++++++++++++ 4 files changed, 129 insertions(+), 1 deletion(-) create mode 100644 diana_services_api/app/routers/llm.py create mode 100644 diana_services_api/schema/llm_requests.py diff --git a/diana_services_api/app/__init__.py b/diana_services_api/app/__init__.py index 5bb95ef..8a83bbf 100644 --- a/diana_services_api/app/__init__.py +++ b/diana_services_api/app/__init__.py @@ -28,6 +28,7 @@ from diana_services_api.app.dependencies import client_manager, jwt_bearer, mq_connector from diana_services_api.app.routers.api_proxy import proxy_route +from diana_services_api.app.routers.llm import llm_route from diana_services_api.app.routers.mq_backend import mq_route from diana_services_api.app.routers.auth import auth_route from diana_services_api.version import __version__ @@ -41,5 +42,6 @@ def create_app(config: dict): app.include_router(auth_route) app.include_router(proxy_route) app.include_router(mq_route) + app.include_router(llm_route) return app diff --git a/diana_services_api/app/routers/llm.py b/diana_services_api/app/routers/llm.py new file mode 100644 index 0000000..9c5e575 --- /dev/null +++ b/diana_services_api/app/routers/llm.py @@ -0,0 +1,58 @@ +# NEON AI (TM) SOFTWARE, Software Development Kit & Application Development System +# All trademark and other rights reserved by their respective owners +# Copyright 2008-2021 Neongecko.com Inc. +# BSD-3 +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from this +# software without specific prior written permission. +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +# OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +from fastapi import APIRouter, Depends +from diana_services_api.schema.llm_requests import * +from diana_services_api.app.dependencies import jwt_bearer, mq_connector + + +llm_route = APIRouter(prefix="/llm", tags=["backend"], + dependencies=[Depends(jwt_bearer)]) + + +@llm_route.post("/chatgpt") +async def llm_ask_chatgpt(query: LLMRequest) -> LLMResponse: + return mq_connector.query_llm("chat_gpt", **dict(query)) + + +@llm_route.post("/fastchat") +async def llm_ask_fastchat(query: LLMRequest) -> LLMResponse: + return mq_connector.query_llm("fastchat", **dict(query)) + + +@llm_route.post("/gemini") +async def llm_ask_gemini(query: LLMRequest) -> LLMResponse: + return mq_connector.query_llm("gemini", **dict(query)) + + +@llm_route.post("/claude") +async def llm_ask_claude(query: LLMRequest) -> LLMResponse: + return mq_connector.query_llm("claude", **dict(query)) + + +@llm_route.post("/palm") +async def llm_ask_palm(query: LLMRequest) -> LLMResponse: + return mq_connector.query_llm("palm2", **dict(query)) diff --git a/diana_services_api/mq_service_api.py b/diana_services_api/mq_service_api.py index 84dbc20..224abfa 100644 --- a/diana_services_api/mq_service_api.py +++ b/diana_services_api/mq_service_api.py @@ -25,7 +25,8 @@ # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. import json -from typing import Optional, Dict, Any +from typing import Optional, Dict, Any, List +from uuid import uuid4 from fastapi import HTTPException @@ -41,6 +42,7 @@ class APIError(HTTPException): class MQServiceManager: def __init__(self, config: dict): self.mq_default_timeout = config.get('mq_default_timeout', 10) + self.mq_cliend_id = str(uuid4()) def _validate_api_proxy_response(self, response: dict): if response['status_code'] == 200: @@ -67,6 +69,18 @@ def query_api_proxy(self, service_name: str, query_params: dict, "neon_api_output", timeout) return self._validate_api_proxy_response(response) + def query_llm(self, llm_name: str, query: str, history: List[tuple]): + response = send_mq_request("/llm", {"query": query, + "history": history}, + f"{llm_name}_input", + response_queue=f"{llm_name}_" + f"{self.mq_cliend_id}") + response = response.get('response') or "" + history.append(("user", query)) + history.append(("llm", response)) + return {"response": response, + "history": history} + def send_email(self, recipient: str, subject: str, body: str, attachments: Optional[Dict[str, str]]): request_data = {"recipient": recipient, diff --git a/diana_services_api/schema/llm_requests.py b/diana_services_api/schema/llm_requests.py new file mode 100644 index 0000000..1861f4d --- /dev/null +++ b/diana_services_api/schema/llm_requests.py @@ -0,0 +1,54 @@ +# NEON AI (TM) SOFTWARE, Software Development Kit & Application Development System +# All trademark and other rights reserved by their respective owners +# Copyright 2008-2021 Neongecko.com Inc. +# BSD-3 +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from this +# software without specific prior written permission. +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +# OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +from typing import List + +from pydantic import BaseModel + + +class LLMRequest(BaseModel): + query: str + history: List[tuple] = [] + model_config = { + "json_schema_extra": { + "examples": [{ + "query": "I am well, how about you?", + "history": [("user", "hello"), + ("llm", "Hi, how can I help you today?")]}]}} + + +class LLMResponse(BaseModel): + response: str + history: List[tuple] + model_config = { + "json_schema_extra": { + "examples": [{ + "query": "I am well, how about you?", + "history": [("user", "hello"), + ("llm", "Hi, how can I help you today?"), + ("user", "I am well, how about you?"), + ("llm", "As a large language model, I do not feel") + ]}]}} From 3ce54b44bbb94fb640b0a11abb31d85b9d381518 Mon Sep 17 00:00:00 2001 From: Daniel McKnight Date: Tue, 16 Jan 2024 17:14:43 -0800 Subject: [PATCH 08/17] Implement configuration aod Docker with default overlay --- Dockerfile | 16 +++++++++++++ diana_services_api/app/__init__.py | 4 ++-- diana_services_api/app/__main__.py | 8 ++++--- diana_services_api/app/dependencies.py | 4 +++- diana_services_api/auth/client_manager.py | 9 ++++---- diana_services_api/mq_service_api.py | 2 +- docker_overlay/config/neon/.keep | 0 docker_overlay/etc/neon/diana.yaml | 28 +++++++++++++++++++++++ requirements/requirements.txt | 3 ++- setup.py | 2 +- 10 files changed, 63 insertions(+), 13 deletions(-) create mode 100644 Dockerfile create mode 100644 docker_overlay/config/neon/.keep create mode 100644 docker_overlay/etc/neon/diana.yaml diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..aeb839d --- /dev/null +++ b/Dockerfile @@ -0,0 +1,16 @@ +FROM python:3.9-slim + +LABEL vendor=neon.ai \ + ai.neon.name="diana-services-api" + +ENV OVOS_CONFIG_BASE_FOLDER neon +ENV OVOS_CONFIG_FILENAME diana.yaml +ENV XDG_CONFIG_HOME /config + +COPY docker_overlay/ / + +WORKDIR /app +COPY . /app +RUN pip install /app + +CMD ["python3", "/app/diana_services_api/app/__main__.py"] \ No newline at end of file diff --git a/diana_services_api/app/__init__.py b/diana_services_api/app/__init__.py index 8a83bbf..d251d06 100644 --- a/diana_services_api/app/__init__.py +++ b/diana_services_api/app/__init__.py @@ -35,8 +35,8 @@ def create_app(config: dict): - title = config.get('title') or "Diana Services API" - summary = config.get('summary') or "HTTP component of the Device Independent API for Neon Applications (DIANA)" + title = config.get('fastapi_title') or "Diana Services API" + summary = config.get('fastapi_summary') or "" version = __version__ app = FastAPI(title=title, summary=summary, version=version) app.include_router(auth_route) diff --git a/diana_services_api/app/__main__.py b/diana_services_api/app/__main__.py index 98a1105..b975618 100644 --- a/diana_services_api/app/__main__.py +++ b/diana_services_api/app/__main__.py @@ -26,14 +26,16 @@ import uvicorn +from ovos_config.config import Configuration + from diana_services_api.app import create_app def main(): - config = dict() + config = Configuration().get("diana_services_api") app = create_app(config) - # TODO: host, port from config - uvicorn.run(app, host="0.0.0.0", port=8080) + uvicorn.run(app, host=config.get('server_host', "0.0.0.0"), + port=config.get('port', 8080)) if __name__ == "__main__": diff --git a/diana_services_api/app/dependencies.py b/diana_services_api/app/dependencies.py index 926a177..720a99c 100644 --- a/diana_services_api/app/dependencies.py +++ b/diana_services_api/app/dependencies.py @@ -24,10 +24,12 @@ # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +from ovos_config.config import Configuration + from diana_services_api.mq_service_api import MQServiceManager from diana_services_api.auth.client_manager import ClientManager, UserTokenAuth -config = dict() # TODO +config = Configuration().get("diana_services_api") or dict() mq_connector = MQServiceManager(config) client_manager = ClientManager(config) jwt_bearer = UserTokenAuth(client_manager) diff --git a/diana_services_api/auth/client_manager.py b/diana_services_api/auth/client_manager.py index ded3f0c..3133491 100644 --- a/diana_services_api/auth/client_manager.py +++ b/diana_services_api/auth/client_manager.py @@ -39,10 +39,9 @@ def __init__(self, config: dict): self._access_token_lifetime = config.get("access_token_ttl", 3600 * 24) self._refresh_token_lifetime = config.get("refresh_token_ttl", 3600 * 24 * 7) - self._access_secret = config.get("access_token_secret", - 'a800445648142061fc238d1f84e96200da87f4f9f784108ac90db8b4391b117b') - self._refresh_secret = config.get("refresh_token_secret", - '833d369ac73d883123743a44b4a7fe21203cffc956f4c8a99be6e71aafa8e1aa') + self._access_secret = config.get("access_token_secret") + self._refresh_secret = config.get("refresh_token_secret") + self._disable_auth = config.get("disable_auth") self._jwt_algo = "HS256" def check_auth_request(self, client_id: str, username: str, @@ -69,6 +68,8 @@ def check_auth_request(self, client_id: str, username: str, return auth def validate_auth(self, token: str) -> bool: + if self._disable_auth: + return True try: auth = jwt.decode(token, self._access_secret, self._jwt_algo) if auth['expire'] < time(): diff --git a/diana_services_api/mq_service_api.py b/diana_services_api/mq_service_api.py index 224abfa..925b1c4 100644 --- a/diana_services_api/mq_service_api.py +++ b/diana_services_api/mq_service_api.py @@ -42,7 +42,7 @@ class APIError(HTTPException): class MQServiceManager: def __init__(self, config: dict): self.mq_default_timeout = config.get('mq_default_timeout', 10) - self.mq_cliend_id = str(uuid4()) + self.mq_cliend_id = config.get('mq_client_id') or str(uuid4()) def _validate_api_proxy_response(self, response: dict): if response['status_code'] == 200: diff --git a/docker_overlay/config/neon/.keep b/docker_overlay/config/neon/.keep new file mode 100644 index 0000000..e69de29 diff --git a/docker_overlay/etc/neon/diana.yaml b/docker_overlay/etc/neon/diana.yaml new file mode 100644 index 0000000..eb972ca --- /dev/null +++ b/docker_overlay/etc/neon/diana.yaml @@ -0,0 +1,28 @@ +log_level: INFO +logs: + level_overrides: + error: + - pika + warning: + - filelock + info: + - openai + debug: [] +MQ: + server: neon-rabbitmq + port: 5672 + users: + mq_handler: + user: neon_api_utils + password: Klatchat2021 +diana_services_api: + mq_default_timeout: 10 + access_token_ttl: 86400 # 1 day + refresh_token_ttl: 604800 # 1 week + access_token_secret: a800445648142061fc238d1f84e96200da87f4f9f784108ac90db8b4391b117b + refresh_token_secret: 833d369ac73d883123743a44b4a7fe21203cffc956f4c8a99be6e71aafa8e1aa + server_host: "0.0.0.0" + server_port: 8080 + fastapi_title: "Diana Services API" + fastapi_summary: "HTTP component of the Device Independent API for Neon Applications (DIANA)" + disable_auth: True \ No newline at end of file diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 24302ce..9b7bc2c 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -3,4 +3,5 @@ fastapi~=0.95 uvicorn~=0.25 pydantic~=2.5 pyjwt~=2.8 -neon_mq_connector~=0.7 \ No newline at end of file +neon-mq-connector~=0.7 +ovos-config~=0.0.12 \ No newline at end of file diff --git a/setup.py b/setup.py index 192af02..3a4f814 100644 --- a/setup.py +++ b/setup.py @@ -27,7 +27,7 @@ # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. from setuptools import setup, find_packages -from os import getenv, path, walk +from os import getenv, path BASE_PATH = path.abspath(path.dirname(__file__)) From 0b7197d50f5aa25ee913186385e3945cea1cf3dd Mon Sep 17 00:00:00 2001 From: Daniel McKnight Date: Thu, 18 Jan 2024 15:26:28 -0800 Subject: [PATCH 09/17] Implement JWT token refresh --- diana_services_api/app/__main__.py | 2 +- diana_services_api/app/routers/auth.py | 5 +++ diana_services_api/auth/client_manager.py | 51 ++++++++++++++++++---- diana_services_api/schema/auth_requests.py | 6 +++ 4 files changed, 55 insertions(+), 9 deletions(-) diff --git a/diana_services_api/app/__main__.py b/diana_services_api/app/__main__.py index b975618..59bf4a6 100644 --- a/diana_services_api/app/__main__.py +++ b/diana_services_api/app/__main__.py @@ -32,7 +32,7 @@ def main(): - config = Configuration().get("diana_services_api") + config = Configuration().get("diana_services_api", {}) app = create_app(config) uvicorn.run(app, host=config.get('server_host', "0.0.0.0"), port=config.get('port', 8080)) diff --git a/diana_services_api/app/routers/auth.py b/diana_services_api/app/routers/auth.py index 6d1b8f1..374b9e8 100644 --- a/diana_services_api/app/routers/auth.py +++ b/diana_services_api/app/routers/auth.py @@ -35,3 +35,8 @@ @auth_route.post("/login") async def check_login(request: AuthenticationRequest) -> AuthenticationResponse: return client_manager.check_auth_request(**dict(request)) + + +@auth_route.post("/refresh") +async def check_refresh(request: RefreshRequest) -> AuthenticationResponse: + return client_manager.check_refresh_request(**dict(request)) diff --git a/diana_services_api/auth/client_manager.py b/diana_services_api/auth/client_manager.py index 3133491..ac39ecb 100644 --- a/diana_services_api/auth/client_manager.py +++ b/diana_services_api/auth/client_manager.py @@ -44,6 +44,17 @@ def __init__(self, config: dict): self._disable_auth = config.get("disable_auth") self._jwt_algo = "HS256" + def _create_tokens(self, encode_data: dict) -> dict: + token = jwt.encode(encode_data, self._access_secret, self._jwt_algo) + encode_data['expire'] = time() + self._refresh_token_lifetime + encode_data['access_token'] = token + refresh = jwt.encode(encode_data, self._refresh_secret, self._jwt_algo) + # TODO: Store refresh token on server to allow invalidating clients + return {"username": encode_data['username'], + "client_id": encode_data['client_id'], + "access_token": token, + "refresh_token": refresh} + def check_auth_request(self, client_id: str, username: str, password: Optional[str] = None): if client_id in self.authorized_clients: @@ -56,17 +67,41 @@ def check_auth_request(self, client_id: str, username: str, "username": username, "password": password, "expire": expiration} - token = jwt.encode(encode_data, self._access_secret, self._jwt_algo) - encode_data['expire'] = time() + self._refresh_token_lifetime - refresh = jwt.encode(encode_data, self._refresh_secret, self._jwt_algo) - # TODO: Store refresh token on server to validate refresh requests - auth = {"username": username, - "client_id": client_id, - "access_token": token, - "refresh_token": refresh} + auth = self._create_tokens(encode_data) self.authorized_clients[client_id] = auth return auth + def check_refresh_request(self, access_token: str, refresh_token: str, + client_id: str): + # Read and validate refresh token + try: + refresh_data = jwt.decode(refresh_token, self._refresh_secret, + self._jwt_algo) + except DecodeError: + raise HTTPException(status_code=400, + detail="Invalid refresh token supplied") + if refresh_data['access_token'] != access_token: + raise HTTPException(status_code=403, + detail="Refresh and access token mismatch") + if time() > refresh_data['expire']: + raise HTTPException(status_code=401, + detail="Refresh token is expired") + # Read access token and re-generate a new pair of tokens + try: + token_data = jwt.decode(access_token, self._access_secret, + self._jwt_algo) + except DecodeError: + raise HTTPException(status_code=400, + detail="Invalid access token supplied") + if token_data['client_id'] != client_id: + raise HTTPException(status_code=403, + detail="Access token does not match client_id") + encode_data = {k: token_data[k] for k in + ("client_id", "username", "password")} + encode_data["expire"] = time() + self._access_token_lifetime + new_auth = self._create_tokens(encode_data) + return new_auth + def validate_auth(self, token: str) -> bool: if self._disable_auth: return True diff --git a/diana_services_api/schema/auth_requests.py b/diana_services_api/schema/auth_requests.py index 3c58df0..eef7dfb 100644 --- a/diana_services_api/schema/auth_requests.py +++ b/diana_services_api/schema/auth_requests.py @@ -57,3 +57,9 @@ class AuthenticationResponse(BaseModel): "access_token": "", "refresh_token": "" }]}} + + +class RefreshRequest(BaseModel): + access_token: str + refresh_token: str + client_id: str From 1370058728307000e91c7ff5efa2e92fe36d0ddf Mon Sep 17 00:00:00 2001 From: Daniel McKnight Date: Thu, 18 Jan 2024 18:33:11 -0800 Subject: [PATCH 10/17] Implement rate limiting --- diana_services_api/auth/client_manager.py | 24 +++++++++++++++++++---- requirements/requirements.txt | 1 + 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/diana_services_api/auth/client_manager.py b/diana_services_api/auth/client_manager.py index ac39ecb..58b7829 100644 --- a/diana_services_api/auth/client_manager.py +++ b/diana_services_api/auth/client_manager.py @@ -31,16 +31,21 @@ from fastapi import Request, HTTPException from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials from jwt import DecodeError +from token_throttler import TokenThrottler, TokenBucket +from token_throttler.storage import RuntimeStorage class ClientManager: def __init__(self, config: dict): + self.rate_limiter = TokenThrottler(cost=1, storage=RuntimeStorage()) + self.authorized_clients: Dict[str, dict] = dict() self._access_token_lifetime = config.get("access_token_ttl", 3600 * 24) self._refresh_token_lifetime = config.get("refresh_token_ttl", 3600 * 24 * 7) - self._access_secret = config.get("access_token_secret") - self._refresh_secret = config.get("refresh_token_secret") + self._access_secret = config.get("access_token_secret") or "a800445648142061fc238d1f84e96200da87f4f9f784108ac90db8b4391b117b" + self._refresh_secret = config.get("refresh_token_secret") or "a800445648142061fc238d1f84e96200da87f4f9f784108ac90db8b4391b117b" + self._rpm = config.get("requests_per_minute", 60) self._disable_auth = config.get("disable_auth") self._jwt_algo = "HS256" @@ -102,7 +107,16 @@ def check_refresh_request(self, access_token: str, refresh_token: str, new_auth = self._create_tokens(encode_data) return new_auth - def validate_auth(self, token: str) -> bool: + def validate_auth(self, token: str, origin_ip: str) -> bool: + if not self.rate_limiter.get_all_buckets(origin_ip): + self.rate_limiter.add_bucket(origin_ip, + TokenBucket(replenish_time=60, + max_tokens=self._rpm)) + if not self.rate_limiter.consume(origin_ip) and self._rpm > 0: + raise HTTPException(status_code=429, + detail=f"Requests limited to {self._rpm}/min" + f"per client connection") + if self._disable_auth: return True try: @@ -112,6 +126,7 @@ def validate_auth(self, token: str) -> bool: return False # Keep track of authorized client connections self.authorized_clients[auth['client_id']] = auth + # TODO: Consider consuming an extra request for guest sessions return True except DecodeError: # Invalid token supplied @@ -131,7 +146,8 @@ async def __call__(self, request: Request): if not credentials.scheme == "Bearer": raise HTTPException(status_code=403, detail="Invalid authentication scheme.") - if not self.client_manager.validate_auth(credentials.credentials): + if not self.client_manager.validate_auth(credentials.credentials, + request.client.host): raise HTTPException(status_code=403, detail="Invalid or expired token.") return credentials.credentials diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 9b7bc2c..1948175 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -3,5 +3,6 @@ fastapi~=0.95 uvicorn~=0.25 pydantic~=2.5 pyjwt~=2.8 +token-throttler~=1.4 neon-mq-connector~=0.7 ovos-config~=0.0.12 \ No newline at end of file From 4c2bfc932ca59e344af0f01b2b4b6e7730b13864 Mon Sep 17 00:00:00 2001 From: Daniel McKnight Date: Thu, 18 Jan 2024 18:56:48 -0800 Subject: [PATCH 11/17] Refactor to `hana` and add documentation --- .github/workflows/propose_release.yml | 2 +- .github/workflows/publish_test_build.yml | 2 +- Dockerfile | 4 +- README.md | 47 +++++++++++++++++-- docker_overlay/etc/neon/diana.yaml | 8 ++-- {diana_services_api => neon_hana}/__init__.py | 0 .../app/__init__.py | 14 +++--- .../app/__main__.py | 4 +- .../app/dependencies.py | 6 +-- .../app/routers/__init__.py | 0 .../app/routers/api_proxy.py | 6 +-- .../app/routers/auth.py | 4 +- .../app/routers/llm.py | 4 +- .../app/routers/mq_backend.py | 6 +-- .../auth/__init__.py | 0 .../auth/client_manager.py | 0 .../mq_service_api.py | 4 +- .../schema/__init__.py | 0 .../schema/api_requests.py | 0 .../schema/api_responses.py | 0 .../schema/auth_requests.py | 0 .../schema/llm_requests.py | 0 {diana_services_api => neon_hana}/version.py | 0 setup.py | 8 ++-- tests/test_auth.py | 2 +- 25 files changed, 79 insertions(+), 42 deletions(-) rename {diana_services_api => neon_hana}/__init__.py (100%) rename {diana_services_api => neon_hana}/app/__init__.py (82%) rename {diana_services_api => neon_hana}/app/__main__.py (94%) rename {diana_services_api => neon_hana}/app/dependencies.py (90%) rename {diana_services_api => neon_hana}/app/routers/__init__.py (100%) rename {diana_services_api => neon_hana}/app/routers/api_proxy.py (94%) rename {diana_services_api => neon_hana}/app/routers/auth.py (94%) rename {diana_services_api => neon_hana}/app/routers/llm.py (95%) rename {diana_services_api => neon_hana}/app/routers/mq_backend.py (93%) rename {diana_services_api => neon_hana}/auth/__init__.py (100%) rename {diana_services_api => neon_hana}/auth/client_manager.py (100%) rename {diana_services_api => neon_hana}/mq_service_api.py (97%) rename {diana_services_api => neon_hana}/schema/__init__.py (100%) rename {diana_services_api => neon_hana}/schema/api_requests.py (100%) rename {diana_services_api => neon_hana}/schema/api_responses.py (100%) rename {diana_services_api => neon_hana}/schema/auth_requests.py (100%) rename {diana_services_api => neon_hana}/schema/llm_requests.py (100%) rename {diana_services_api => neon_hana}/version.py (100%) diff --git a/.github/workflows/propose_release.yml b/.github/workflows/propose_release.yml index eeb6de6..68d6440 100644 --- a/.github/workflows/propose_release.yml +++ b/.github/workflows/propose_release.yml @@ -16,7 +16,7 @@ jobs: branch: dev release_type: ${{ inputs.release_type }} update_changelog: True - version_file: "neon_diana_utils/version.py" + version_file: "neon_hana/version.py" on_version_change: "scripts/sync_chart_app_version.py" pull_changes: uses: neongeckocom/.github/.github/workflows/pull_master.yml@master diff --git a/.github/workflows/publish_test_build.yml b/.github/workflows/publish_test_build.yml index d871d93..26d8bde 100644 --- a/.github/workflows/publish_test_build.yml +++ b/.github/workflows/publish_test_build.yml @@ -6,7 +6,7 @@ on: branches: - dev paths-ignore: - - 'neon_diana_utils/version.py' + - 'neon_hana/version.py' - '**/Chart.yaml' jobs: diff --git a/Dockerfile b/Dockerfile index aeb839d..f8095c0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,7 @@ FROM python:3.9-slim LABEL vendor=neon.ai \ - ai.neon.name="diana-services-api" + ai.neon.name="neon-hana" ENV OVOS_CONFIG_BASE_FOLDER neon ENV OVOS_CONFIG_FILENAME diana.yaml @@ -13,4 +13,4 @@ WORKDIR /app COPY . /app RUN pip install /app -CMD ["python3", "/app/diana_services_api/app/__main__.py"] \ No newline at end of file +CMD ["python3", "/app/neon_hana/app/__main__.py"] \ No newline at end of file diff --git a/README.md b/README.md index 00064e9..f1c130f 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,42 @@ -# Diana Services API -This package provides an HTTP front-end for accessing services in a -[Neon DIANA](https://github.com/NeonGeckoCom/neon-diana-utils) deployment. This -should generally be hosted as part of a Diana deployment to safely expose services -via HTTP. +# HANA +HANA (HTTP API for Neon Applications) provides a unified front-end for +accessing services in a [Neon DIANA](https://github.com/NeonGeckoCom/neon-diana-utils) deployment. This API should generally +be hosted as part of a Diana deployment to safely expose services to outside +traffic. + +Full API documentation is automatically generated and accessible at `/docs`. + +## Configuration +User configuration belongs in `diana.yaml`, mounted in the container path +`/config/neon/`. An example user configuration could be: +```yaml +MQ: + server: mq.mydomain.com +hana: + mq_default_timeout: 10 + access_token_ttl: 86400 # 1 day + refresh_token_ttl: 604800 # 1 week + requests_per_minute: 60 + access_token_secret: a800445648142061fc238d1f84e96200da87f4f9fa7835cac90db8b4391b117b + refresh_token_secret: 833d369ac73d883123743a44b4a7fe21203cffc956f4c8fec712e71aafa8e1aa + fastapi_title: "My HANA API Host" + fastapi_summary: "Personal HTTP API to access my DIANA backend." + disable_auth: True +``` +It is recommended to generate unique values for configured tokens, these are 32 +bytes in hexadecimal representation. + +## Deployment +You can build a Docker container from this repository, or pull a built container +from the GitHub Container Registry. Start Hana via: +```shell +docker run -p 8080:8080 -v ~/.config/neon:/config/neon ghcr.io/neongeckocom/neon-hana +``` +> This assumes you have configuration defined in `~/.config/neon/diana.yaml` and + are using the default port 8080 + +## Usage +Full API documentation is available at `/docs`. The `/auth/login` endpoint should +be used to generate a `client_id`, `access_token`, and `refresh_token`. The +`access_token` should be included in every request and upon expiration of the +`access_token`, a new token can be obtained from the `auth/refresh` endpoint. diff --git a/docker_overlay/etc/neon/diana.yaml b/docker_overlay/etc/neon/diana.yaml index eb972ca..8c9821b 100644 --- a/docker_overlay/etc/neon/diana.yaml +++ b/docker_overlay/etc/neon/diana.yaml @@ -15,14 +15,14 @@ MQ: mq_handler: user: neon_api_utils password: Klatchat2021 -diana_services_api: +hana: mq_default_timeout: 10 access_token_ttl: 86400 # 1 day refresh_token_ttl: 604800 # 1 week + requests_per_minute: 60 access_token_secret: a800445648142061fc238d1f84e96200da87f4f9f784108ac90db8b4391b117b refresh_token_secret: 833d369ac73d883123743a44b4a7fe21203cffc956f4c8a99be6e71aafa8e1aa server_host: "0.0.0.0" server_port: 8080 - fastapi_title: "Diana Services API" - fastapi_summary: "HTTP component of the Device Independent API for Neon Applications (DIANA)" - disable_auth: True \ No newline at end of file + fastapi_title: "HANA: HTTP API for Neon Applications" + fastapi_summary: "HTTP component of the Device Independent API for Neon Applications (DIANA)" \ No newline at end of file diff --git a/diana_services_api/__init__.py b/neon_hana/__init__.py similarity index 100% rename from diana_services_api/__init__.py rename to neon_hana/__init__.py diff --git a/diana_services_api/app/__init__.py b/neon_hana/app/__init__.py similarity index 82% rename from diana_services_api/app/__init__.py rename to neon_hana/app/__init__.py index d251d06..8d6c9b1 100644 --- a/diana_services_api/app/__init__.py +++ b/neon_hana/app/__init__.py @@ -26,16 +26,16 @@ from fastapi import FastAPI -from diana_services_api.app.dependencies import client_manager, jwt_bearer, mq_connector -from diana_services_api.app.routers.api_proxy import proxy_route -from diana_services_api.app.routers.llm import llm_route -from diana_services_api.app.routers.mq_backend import mq_route -from diana_services_api.app.routers.auth import auth_route -from diana_services_api.version import __version__ +from neon_hana.app.dependencies import client_manager, jwt_bearer, mq_connector +from neon_hana.app.routers.api_proxy import proxy_route +from neon_hana.app.routers.llm import llm_route +from neon_hana.app.routers.mq_backend import mq_route +from neon_hana.app.routers.auth import auth_route +from neon_hana.version import __version__ def create_app(config: dict): - title = config.get('fastapi_title') or "Diana Services API" + title = config.get('fastapi_title') or "HANA: HTTP API for Neon Applications" summary = config.get('fastapi_summary') or "" version = __version__ app = FastAPI(title=title, summary=summary, version=version) diff --git a/diana_services_api/app/__main__.py b/neon_hana/app/__main__.py similarity index 94% rename from diana_services_api/app/__main__.py rename to neon_hana/app/__main__.py index 59bf4a6..d004f9b 100644 --- a/diana_services_api/app/__main__.py +++ b/neon_hana/app/__main__.py @@ -28,11 +28,11 @@ from ovos_config.config import Configuration -from diana_services_api.app import create_app +from neon_hana.app import create_app def main(): - config = Configuration().get("diana_services_api", {}) + config = Configuration().get("hana", {}) app = create_app(config) uvicorn.run(app, host=config.get('server_host', "0.0.0.0"), port=config.get('port', 8080)) diff --git a/diana_services_api/app/dependencies.py b/neon_hana/app/dependencies.py similarity index 90% rename from diana_services_api/app/dependencies.py rename to neon_hana/app/dependencies.py index 720a99c..0c9dcf5 100644 --- a/diana_services_api/app/dependencies.py +++ b/neon_hana/app/dependencies.py @@ -26,10 +26,10 @@ from ovos_config.config import Configuration -from diana_services_api.mq_service_api import MQServiceManager -from diana_services_api.auth.client_manager import ClientManager, UserTokenAuth +from neon_hana.mq_service_api import MQServiceManager +from neon_hana.auth.client_manager import ClientManager, UserTokenAuth -config = Configuration().get("diana_services_api") or dict() +config = Configuration().get("hana") or dict() mq_connector = MQServiceManager(config) client_manager = ClientManager(config) jwt_bearer = UserTokenAuth(client_manager) diff --git a/diana_services_api/app/routers/__init__.py b/neon_hana/app/routers/__init__.py similarity index 100% rename from diana_services_api/app/routers/__init__.py rename to neon_hana/app/routers/__init__.py diff --git a/diana_services_api/app/routers/api_proxy.py b/neon_hana/app/routers/api_proxy.py similarity index 94% rename from diana_services_api/app/routers/api_proxy.py rename to neon_hana/app/routers/api_proxy.py index d263338..de37194 100644 --- a/diana_services_api/app/routers/api_proxy.py +++ b/neon_hana/app/routers/api_proxy.py @@ -25,9 +25,9 @@ # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. from fastapi import APIRouter, Depends -from diana_services_api.schema.api_requests import * -from diana_services_api.schema.api_responses import * -from diana_services_api.app.dependencies import jwt_bearer, mq_connector +from neon_hana.schema.api_requests import * +from neon_hana.schema.api_responses import * +from neon_hana.app.dependencies import jwt_bearer, mq_connector proxy_route = APIRouter(prefix="/proxy", tags=["backend"], diff --git a/diana_services_api/app/routers/auth.py b/neon_hana/app/routers/auth.py similarity index 94% rename from diana_services_api/app/routers/auth.py rename to neon_hana/app/routers/auth.py index 374b9e8..5145f69 100644 --- a/diana_services_api/app/routers/auth.py +++ b/neon_hana/app/routers/auth.py @@ -26,8 +26,8 @@ from fastapi import APIRouter -from diana_services_api.app.dependencies import client_manager -from diana_services_api.schema.auth_requests import * +from neon_hana.app.dependencies import client_manager +from neon_hana.schema.auth_requests import * auth_route = APIRouter(prefix="/auth", tags=["authentication"]) diff --git a/diana_services_api/app/routers/llm.py b/neon_hana/app/routers/llm.py similarity index 95% rename from diana_services_api/app/routers/llm.py rename to neon_hana/app/routers/llm.py index 9c5e575..a18d86f 100644 --- a/diana_services_api/app/routers/llm.py +++ b/neon_hana/app/routers/llm.py @@ -25,8 +25,8 @@ # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. from fastapi import APIRouter, Depends -from diana_services_api.schema.llm_requests import * -from diana_services_api.app.dependencies import jwt_bearer, mq_connector +from neon_hana.schema.llm_requests import * +from neon_hana.app.dependencies import jwt_bearer, mq_connector llm_route = APIRouter(prefix="/llm", tags=["backend"], diff --git a/diana_services_api/app/routers/mq_backend.py b/neon_hana/app/routers/mq_backend.py similarity index 93% rename from diana_services_api/app/routers/mq_backend.py rename to neon_hana/app/routers/mq_backend.py index 1d11546..6430dff 100644 --- a/diana_services_api/app/routers/mq_backend.py +++ b/neon_hana/app/routers/mq_backend.py @@ -25,9 +25,9 @@ # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. from fastapi import APIRouter, Depends -from diana_services_api.schema.api_requests import * -from diana_services_api.schema.api_responses import * -from diana_services_api.app.dependencies import jwt_bearer, mq_connector +from neon_hana.schema.api_requests import * +from neon_hana.schema.api_responses import * +from neon_hana.app.dependencies import jwt_bearer, mq_connector mq_route = APIRouter(tags=["backend"], dependencies=[Depends(jwt_bearer)]) diff --git a/diana_services_api/auth/__init__.py b/neon_hana/auth/__init__.py similarity index 100% rename from diana_services_api/auth/__init__.py rename to neon_hana/auth/__init__.py diff --git a/diana_services_api/auth/client_manager.py b/neon_hana/auth/client_manager.py similarity index 100% rename from diana_services_api/auth/client_manager.py rename to neon_hana/auth/client_manager.py diff --git a/diana_services_api/mq_service_api.py b/neon_hana/mq_service_api.py similarity index 97% rename from diana_services_api/mq_service_api.py rename to neon_hana/mq_service_api.py index 925b1c4..642beae 100644 --- a/diana_services_api/mq_service_api.py +++ b/neon_hana/mq_service_api.py @@ -125,7 +125,7 @@ def get_stt(self, b64_audio: str, lang: str, timeout: int = 20): "data": {"audio_data": b64_audio, "utterances": [""], # TODO: Compat "lang": lang}, - "context": {"source": "diana_services_api"}} + "context": {"source": "hana"}} response = send_mq_request("/neon_chat_api", request_data, "neon_chat_api_request", timeout=timeout) return response @@ -138,7 +138,7 @@ def get_tts(self, string: str, lang: str, gender: str, timeout: int = 20): "gender": gender, "lang": lang}, "lang": lang}, - "context": {"source": "diana_services_api"}} + "context": {"source": "hana"}} response = send_mq_request("/neon_chat_api", request_data, "neon_chat_api_request", timeout=timeout) return response diff --git a/diana_services_api/schema/__init__.py b/neon_hana/schema/__init__.py similarity index 100% rename from diana_services_api/schema/__init__.py rename to neon_hana/schema/__init__.py diff --git a/diana_services_api/schema/api_requests.py b/neon_hana/schema/api_requests.py similarity index 100% rename from diana_services_api/schema/api_requests.py rename to neon_hana/schema/api_requests.py diff --git a/diana_services_api/schema/api_responses.py b/neon_hana/schema/api_responses.py similarity index 100% rename from diana_services_api/schema/api_responses.py rename to neon_hana/schema/api_responses.py diff --git a/diana_services_api/schema/auth_requests.py b/neon_hana/schema/auth_requests.py similarity index 100% rename from diana_services_api/schema/auth_requests.py rename to neon_hana/schema/auth_requests.py diff --git a/diana_services_api/schema/llm_requests.py b/neon_hana/schema/llm_requests.py similarity index 100% rename from diana_services_api/schema/llm_requests.py rename to neon_hana/schema/llm_requests.py diff --git a/diana_services_api/version.py b/neon_hana/version.py similarity index 100% rename from diana_services_api/version.py rename to neon_hana/version.py diff --git a/setup.py b/setup.py index 3a4f814..eb6c40d 100644 --- a/setup.py +++ b/setup.py @@ -52,7 +52,7 @@ def get_requirements(requirements_filename: str): with open(path.join(BASE_PATH, "README.md"), "r") as f: long_description = f.read() -with open(path.join(BASE_PATH, "diana_services_api", +with open(path.join(BASE_PATH, "hana", "version.py"), "r", encoding="utf-8") as v: for line in v.readlines(): if line.startswith("__version__"): @@ -63,12 +63,12 @@ def get_requirements(requirements_filename: str): setup( - name='diana-services-api', + name='neon-hana', version=version, - description='Web API to access Neon DIANA Services', + description='Web API to access DIANA Services', long_description=long_description, long_description_content_type="text/markdown", - url='https://github.com/NeonGeckoCom/diana-services-api', + url='https://github.com/NeonGeckoCom/neon-hana', author='NeonGecko', author_email='developers@neon.ai', license='BSD-3-Clause', diff --git a/tests/test_auth.py b/tests/test_auth.py index f535005..589234a 100644 --- a/tests/test_auth.py +++ b/tests/test_auth.py @@ -29,7 +29,7 @@ class TestClientManager(unittest.TestCase): - from diana_services_api.auth.client_manager import ClientManager + from neon_hana.auth.client_manager import ClientManager client_manager = ClientManager({}) def test_check_auth_request(self): From 1cf29d3adda8739995b91a95a9a907c560905bc6 Mon Sep 17 00:00:00 2001 From: Daniel McKnight Date: Thu, 18 Jan 2024 19:03:15 -0800 Subject: [PATCH 12/17] Update automation for new repo Fix copy/paste leftovers in setup.py --- .github/workflows/license_tests.yml | 3 ++- .github/workflows/propose_release.yml | 1 - .github/workflows/publish_release.yml | 16 ++++++++++++++-- .github/workflows/publish_test_build.yml | 8 ++++++-- setup.py | 2 +- 5 files changed, 23 insertions(+), 7 deletions(-) diff --git a/.github/workflows/license_tests.yml b/.github/workflows/license_tests.yml index dcf543d..50b7775 100644 --- a/.github/workflows/license_tests.yml +++ b/.github/workflows/license_tests.yml @@ -7,4 +7,5 @@ on: - master jobs: license_tests: - uses: neongeckocom/.github/.github/workflows/license_tests.yml@master \ No newline at end of file + uses: neongeckocom/.github/.github/workflows/license_tests.yml@master + # TODO \ No newline at end of file diff --git a/.github/workflows/propose_release.yml b/.github/workflows/propose_release.yml index 68d6440..6a3614b 100644 --- a/.github/workflows/propose_release.yml +++ b/.github/workflows/propose_release.yml @@ -17,7 +17,6 @@ jobs: release_type: ${{ inputs.release_type }} update_changelog: True version_file: "neon_hana/version.py" - on_version_change: "scripts/sync_chart_app_version.py" pull_changes: uses: neongeckocom/.github/.github/workflows/pull_master.yml@master needs: update_version diff --git a/.github/workflows/publish_release.yml b/.github/workflows/publish_release.yml index 96a0357..2cd2be9 100644 --- a/.github/workflows/publish_release.yml +++ b/.github/workflows/publish_release.yml @@ -7,6 +7,18 @@ on: - master jobs: - build_and_publish_pypi_and_release: - uses: neongeckocom/.github/.github/workflows/publish_stable_release.yml@master + tag_release: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Get Version + run: | + VERSION=$(python setup.py --version) + echo "VERSION=${VERSION}" >> $GITHUB_ENV + - uses: ncipollo/release-action@v1 + with: + token: ${{secrets.GITHUB_TOKEN}} + tag: ${{env.VERSION}} + build_and_publish_docker: + uses: neongeckocom/.github/.github/workflows/publish_docker.yml@master secrets: inherit \ No newline at end of file diff --git a/.github/workflows/publish_test_build.yml b/.github/workflows/publish_test_build.yml index 26d8bde..63b378c 100644 --- a/.github/workflows/publish_test_build.yml +++ b/.github/workflows/publish_test_build.yml @@ -14,6 +14,10 @@ jobs: uses: neongeckocom/.github/.github/workflows/publish_alpha_release.yml@master secrets: inherit with: - version_file: "neon_diana_utils/version.py" + version_file: "version.py" publish_prerelease: true - on_version_change: "scripts/sync_chart_app_version.py" \ No newline at end of file + publish_pypi: false + build_and_publish_docker: + needs: publish_alpha_release + uses: neongeckocom/.github/.github/workflows/publish_docker.yml@master + secrets: inherit \ No newline at end of file diff --git a/setup.py b/setup.py index eb6c40d..6627dbd 100644 --- a/setup.py +++ b/setup.py @@ -52,7 +52,7 @@ def get_requirements(requirements_filename: str): with open(path.join(BASE_PATH, "README.md"), "r") as f: long_description = f.read() -with open(path.join(BASE_PATH, "hana", +with open(path.join(BASE_PATH, "neon_hana", "version.py"), "r", encoding="utf-8") as v: for line in v.readlines(): if line.startswith("__version__"): From 9ed77436f1107bfef3fc7cefca99618f24db4980 Mon Sep 17 00:00:00 2001 From: Daniel McKnight Date: Thu, 18 Jan 2024 19:06:32 -0800 Subject: [PATCH 13/17] Update excluded licenses Add pythin build tests Update unit test covered versions --- .github/workflows/license_tests.yml | 3 ++- .github/workflows/unit_tests.yml | 6 +++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/.github/workflows/license_tests.yml b/.github/workflows/license_tests.yml index 50b7775..c37ef9c 100644 --- a/.github/workflows/license_tests.yml +++ b/.github/workflows/license_tests.yml @@ -8,4 +8,5 @@ on: jobs: license_tests: uses: neongeckocom/.github/.github/workflows/license_tests.yml@master - # TODO \ No newline at end of file + with: + packages-exclude: '^(neon-hana|dnspython).*' diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml index 53f199d..068ca91 100644 --- a/.github/workflows/unit_tests.yml +++ b/.github/workflows/unit_tests.yml @@ -4,10 +4,14 @@ on: workflow_dispatch: jobs: + py_build_tests: + uses: neongeckocom/.github/.github/workflows/python_build_tests.yml@master + with: + python_version: "3.9" unit_tests: strategy: matrix: - python-version: [ 3.7, 3.8, 3.9, "3.10" ] + python-version: [ 3.9, "3.10", "3.11" ] timeout-minutes: 15 runs-on: ubuntu-latest steps: From c1ccb6d11a73c116ee525e2ccd81001a03fd672c Mon Sep 17 00:00:00 2001 From: Daniel McKnight Date: Thu, 18 Jan 2024 19:10:35 -0800 Subject: [PATCH 14/17] Update tests and remove default token strings --- neon_hana/auth/client_manager.py | 4 ++-- tests/test_auth.py | 14 +++++++++----- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/neon_hana/auth/client_manager.py b/neon_hana/auth/client_manager.py index 58b7829..ce472fc 100644 --- a/neon_hana/auth/client_manager.py +++ b/neon_hana/auth/client_manager.py @@ -43,8 +43,8 @@ def __init__(self, config: dict): self._access_token_lifetime = config.get("access_token_ttl", 3600 * 24) self._refresh_token_lifetime = config.get("refresh_token_ttl", 3600 * 24 * 7) - self._access_secret = config.get("access_token_secret") or "a800445648142061fc238d1f84e96200da87f4f9f784108ac90db8b4391b117b" - self._refresh_secret = config.get("refresh_token_secret") or "a800445648142061fc238d1f84e96200da87f4f9f784108ac90db8b4391b117b" + self._access_secret = config.get("access_token_secret") + self._refresh_secret = config.get("refresh_token_secret") self._rpm = config.get("requests_per_minute", 60) self._disable_auth = config.get("disable_auth") self._jwt_algo = "HS256" diff --git a/tests/test_auth.py b/tests/test_auth.py index 589234a..a9b318e 100644 --- a/tests/test_auth.py +++ b/tests/test_auth.py @@ -30,7 +30,8 @@ class TestClientManager(unittest.TestCase): from neon_hana.auth.client_manager import ClientManager - client_manager = ClientManager({}) + client_manager = ClientManager({"access_token_secret": "a800445648142061fc238d1f84e96200da87f4f9f784108ac90db8b4391b117b", + "refresh_token_secret": "a800445648142061fc238d1f84e96200da87f4f9f784108ac90db8b4391b117b"}) def test_check_auth_request(self): client_1 = str(uuid4()) @@ -63,9 +64,12 @@ def test_validate_auth(self): valid_client = str(uuid4()) invalid_client = str(uuid4()) auth_response = self.client_manager.check_auth_request( - username="valid", client_id=valid_client)['jwt_token'] + username="valid", client_id=valid_client)['access_token'] - self.assertTrue(self.client_manager.validate_auth(auth_response)) - self.assertFalse(self.client_manager.validate_auth(invalid_client)) + self.assertTrue(self.client_manager.validate_auth(auth_response, + "127.0.0.1")) + self.assertFalse(self.client_manager.validate_auth(invalid_client, + "127.0.0.1")) self.client_manager.authorized_clients.pop(valid_client) - self.assertFalse(self.client_manager.validate_auth(auth_response)) + self.assertFalse(self.client_manager.validate_auth(auth_response, + "127.0.0.1")) From 62936036e90555e44300cf19e0a2745a75ae1466 Mon Sep 17 00:00:00 2001 From: Daniel McKnight Date: Fri, 19 Jan 2024 09:31:00 -0800 Subject: [PATCH 15/17] Update auth tests --- tests/test_auth.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/tests/test_auth.py b/tests/test_auth.py index a9b318e..47d7f96 100644 --- a/tests/test_auth.py +++ b/tests/test_auth.py @@ -25,13 +25,17 @@ # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. import unittest +from time import time from uuid import uuid4 +from fastapi import HTTPException + class TestClientManager(unittest.TestCase): from neon_hana.auth.client_manager import ClientManager client_manager = ClientManager({"access_token_secret": "a800445648142061fc238d1f84e96200da87f4f9f784108ac90db8b4391b117b", - "refresh_token_secret": "a800445648142061fc238d1f84e96200da87f4f9f784108ac90db8b4391b117b"}) + "refresh_token_secret": "a800445648142061fc238d1f84e96200da87f4f9f784108ac90db8b4391b117b", + "disable_auth": False}) def test_check_auth_request(self): client_1 = str(uuid4()) @@ -70,6 +74,14 @@ def test_validate_auth(self): "127.0.0.1")) self.assertFalse(self.client_manager.validate_auth(invalid_client, "127.0.0.1")) - self.client_manager.authorized_clients.pop(valid_client) - self.assertFalse(self.client_manager.validate_auth(auth_response, + + expired_token = self.client_manager._create_tokens( + {"client_id": invalid_client, "username": "test", + "password": "test", "expire": time()})['access_token'] + self.assertFalse(self.client_manager.validate_auth(expired_token, "127.0.0.1")) + # TODO: Test rate limited response + + def test_check_refresh_request(self): + # TODO + pass From 4568ee3e31e0becb5f5dabfb55f0bdba09a702c7 Mon Sep 17 00:00:00 2001 From: Daniel McKnight Date: Fri, 19 Jan 2024 11:24:11 -0800 Subject: [PATCH 16/17] Update auth tests --- tests/test_auth.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/test_auth.py b/tests/test_auth.py index 47d7f96..cfc2d39 100644 --- a/tests/test_auth.py +++ b/tests/test_auth.py @@ -80,7 +80,13 @@ def test_validate_auth(self): "password": "test", "expire": time()})['access_token'] self.assertFalse(self.client_manager.validate_auth(expired_token, "127.0.0.1")) - # TODO: Test rate limited response + + self.client_manager._rpm = 1 + self.assertTrue(self.client_manager.validate_auth(auth_response, + "192.168.1.2")) + with self.assertRaises(HTTPException) as e: + self.client_manager.validate_auth(auth_response, "192.168.1.2") + self.assertEqual(e.exception.status_code, 429) def test_check_refresh_request(self): # TODO From 8ca6a081b6ed5b9ef37bde6a27cca83128cdc045 Mon Sep 17 00:00:00 2001 From: Daniel McKnight Date: Fri, 19 Jan 2024 12:26:19 -0800 Subject: [PATCH 17/17] Update documentation and default config Add test coverage for refresh requests with updated handling --- docker_overlay/etc/neon/diana.yaml | 4 +-- neon_hana/auth/client_manager.py | 10 +++--- setup.py | 2 +- tests/test_auth.py | 53 ++++++++++++++++++++++++++++-- 4 files changed, 58 insertions(+), 11 deletions(-) diff --git a/docker_overlay/etc/neon/diana.yaml b/docker_overlay/etc/neon/diana.yaml index 8c9821b..a722370 100644 --- a/docker_overlay/etc/neon/diana.yaml +++ b/docker_overlay/etc/neon/diana.yaml @@ -24,5 +24,5 @@ hana: refresh_token_secret: 833d369ac73d883123743a44b4a7fe21203cffc956f4c8a99be6e71aafa8e1aa server_host: "0.0.0.0" server_port: 8080 - fastapi_title: "HANA: HTTP API for Neon Applications" - fastapi_summary: "HTTP component of the Device Independent API for Neon Applications (DIANA)" \ No newline at end of file + fastapi_title: "Hana" + fastapi_summary: "HANA (HTTP API for Neon Applications) is the HTTP component of the Device Independent API for Neon Applications (DIANA)" \ No newline at end of file diff --git a/neon_hana/auth/client_manager.py b/neon_hana/auth/client_manager.py index ce472fc..ac0d625 100644 --- a/neon_hana/auth/client_manager.py +++ b/neon_hana/auth/client_manager.py @@ -92,12 +92,10 @@ def check_refresh_request(self, access_token: str, refresh_token: str, raise HTTPException(status_code=401, detail="Refresh token is expired") # Read access token and re-generate a new pair of tokens - try: - token_data = jwt.decode(access_token, self._access_secret, - self._jwt_algo) - except DecodeError: - raise HTTPException(status_code=400, - detail="Invalid access token supplied") + # This is already known to be a valid token based on the refresh token + token_data = jwt.decode(access_token, self._access_secret, + self._jwt_algo) + if token_data['client_id'] != client_id: raise HTTPException(status_code=403, detail="Access token does not match client_id") diff --git a/setup.py b/setup.py index 6627dbd..892e74c 100644 --- a/setup.py +++ b/setup.py @@ -65,7 +65,7 @@ def get_requirements(requirements_filename: str): setup( name='neon-hana', version=version, - description='Web API to access DIANA Services', + description='HTTP API for Neon Applications', long_description=long_description, long_description_content_type="text/markdown", url='https://github.com/NeonGeckoCom/neon-hana', diff --git a/tests/test_auth.py b/tests/test_auth.py index cfc2d39..5bec3d9 100644 --- a/tests/test_auth.py +++ b/tests/test_auth.py @@ -89,5 +89,54 @@ def test_validate_auth(self): self.assertEqual(e.exception.status_code, 429) def test_check_refresh_request(self): - # TODO - pass + valid_client = str(uuid4()) + tokens = self.client_manager._create_tokens({"client_id": valid_client, + "username": "test", + "password": "test", + "expire": time()}) + self.assertEqual(tokens['client_id'], valid_client) + + # Test invalid refresh token + with self.assertRaises(HTTPException) as e: + self.client_manager.check_refresh_request(tokens['access_token'], + valid_client, + valid_client) + self.assertEqual(e.exception.status_code, 400) + + # Test incorrect access token + with self.assertRaises(HTTPException) as e: + self.client_manager.check_refresh_request(tokens['refresh_token'], + tokens['refresh_token'], + valid_client) + self.assertEqual(e.exception.status_code, 403) + + # Test invalid client_id + with self.assertRaises(HTTPException) as e: + self.client_manager.check_refresh_request(tokens['access_token'], + tokens['refresh_token'], + str(uuid4())) + self.assertEqual(e.exception.status_code, 403) + + # Test valid refresh + valid_refresh = self.client_manager.check_refresh_request( + tokens['access_token'], tokens['refresh_token'], + tokens['client_id']) + self.assertEqual(valid_refresh['client_id'], tokens['client_id']) + self.assertNotEqual(valid_refresh['access_token'], + tokens['access_token']) + self.assertNotEqual(valid_refresh['refresh_token'], + tokens['refresh_token']) + + # Test expired refresh token + real_refresh = self.client_manager._refresh_token_lifetime + self.client_manager._refresh_token_lifetime = 0 + tokens = self.client_manager._create_tokens({"client_id": valid_client, + "username": "test", + "password": "test", + "expire": time()}) + with self.assertRaises(HTTPException) as e: + self.client_manager.check_refresh_request(tokens['access_token'], + tokens['refresh_token'], + tokens['client_id']) + self.assertEqual(e.exception.status_code, 401) + self.client_manager._refresh_token_lifetime = real_refresh