diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..36af9e4 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,93 @@ +# +# How to build this + +name: Build + +# +# Operational Variables + +env: + MAJOR: 0 + MINOR: 0 + PYTHON_VERSION: 3.13.0 + +# +# Establish when the workflow is run +# We do build on every push except when we push onto main (which ought to be subject to branch protection) +# We do build whenever a PR onto main is closed (see on) and the code is actually merged (see release job if) +# Why is that okay? +# Since we're making a PR, we know from the previous workflow run on push that the repo is okay and the PR +# shows that to us. A PR itself doesn't cause a build, except when it is closed and the changes were merged. + +on: + push: + branches-ignore: + - main + pull_request_target: + branches: + - main + types: + - closed + +# +# Workflow + +jobs: + + build: + runs-on: ubuntu-latest + steps: + + - name: Checkout out our code + uses: actions/checkout@v3 + + - name: Calculate Build Context + run: | + MRMAT_VERSION="${MAJOR}.${MINOR}.${GITHUB_RUN_NUMBER}" + if [ "$GITHUB_EVENT_NAME" == 'pull_request_target' && GITHUB_BASE_REF == 'main']; then + MRMAT_IS_RELEASE=true + echo "::warning ::Building release ${MRMAT_VERSION}" + echo "MRMAT_IS_RELEASE=true" >> $GITHUB_ENV + else + MRMAT_VERSION="${MRMAT_VERSION}.dev0" + echo "::warning ::Building version ${MRMAT_VERSION}" + fi + echo "MRMAT_VERSION=${MRMAT_VERSION}" >> $GITHUB_ENV + + - name: Set up Python ${{ env.PYTHON_VERSION }} + uses: actions/setup-python@v2 + with: + python-version: ${{ env.PYTHON_VERSION }} + + - name: Establish a cache for dependencies + uses: actions/cache@v4 + with: + path: | + ~/.local + ~/.cache/pip + key: ${{ runner.os }} + + - name: Build + run: | + export PYTHONUSERBASE=${HOME}/.local + pip install --user -r requirements.txt -r requirements.dev.txt + PYTHONPATH=${GITHUB_WORKSPACE}/src pytest + PYTHONPATH=${GITHUB_WORKSPACE}/src python -m build --wheel -n + + - name: Upload test results + uses: actions/upload-artifact@v4 + if: ${{ always() }} + with: + name: Test and Coverage + path: | + build/junit.xml + build/coverage.xml + + - name: Conditional Release + uses: marvinpinto/action-automatic-releases@latest + if: (github.event.pull_request.merged == true && github.base_ref == 'main') + with: + repo_token: "${{ secrets.GITHUB_TOKEN }}" + automatic_release_tag: "${{ env.MRMAT_VERSION }}" + prerelease: false + title: "Release ${{ env.MRMAT_VERSION }}" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ba552da --- /dev/null +++ b/.gitignore @@ -0,0 +1,250 @@ + +# Created by https://www.toptal.com/developers/gitignore/api/jetbrains,python +# Edit at https://www.toptal.com/developers/gitignore?templates=jetbrains,python + +### JetBrains ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +### JetBrains Patch ### +# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 + +# *.iml +# modules.xml +# .idea/misc.xml +# *.ipr + +# Sonarlint plugin +# https://plugins.jetbrains.com/plugin/7973-sonarlint +.idea/**/sonarlint/ + +# SonarQube Plugin +# https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin +.idea/**/sonarIssues.xml + +# Markdown Navigator plugin +# https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced +.idea/**/markdown-navigator.xml +.idea/**/markdown-navigator-enh.xml +.idea/**/markdown-navigator/ + +# Cache file creation bug +# See https://youtrack.jetbrains.com/issue/JBR-2257 +.idea/$CACHE_FILE$ + +# CodeStream plugin +# https://plugins.jetbrains.com/plugin/12206-codestream +.idea/codestream.xml + +### Python ### +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +pytestdebug.log + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ +doc/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ +pythonenv* + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# profiling data +.prof + +# End of https://www.toptal.com/developers/gitignore/api/jetbrains,python + + +.coverage diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/copyright/MIT.xml b/.idea/copyright/MIT.xml new file mode 100644 index 0000000..4e0ea94 --- /dev/null +++ b/.idea/copyright/MIT.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/.idea/copyright/profiles_settings.xml b/.idea/copyright/profiles_settings.xml new file mode 100644 index 0000000..c3ba54a --- /dev/null +++ b/.idea/copyright/profiles_settings.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/.idea/dataSources.xml b/.idea/dataSources.xml new file mode 100644 index 0000000..22e22e1 --- /dev/null +++ b/.idea/dataSources.xml @@ -0,0 +1,15 @@ + + + + + sqlite.xerial + true + org.sqlite.JDBC + jdbc:sqlite:$PROJECT_DIR$/build/test.db + + + + $ProjectFileDir$ + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000..105ce2d --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/markdown.xml b/.idea/markdown.xml new file mode 100644 index 0000000..064f873 --- /dev/null +++ b/.idea/markdown.xml @@ -0,0 +1,10 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..be83902 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..e69dc1d --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/mrmat-python-api-fastapi.iml b/.idea/mrmat-python-api-fastapi.iml new file mode 100644 index 0000000..24ec1fd --- /dev/null +++ b/.idea/mrmat-python-api-fastapi.iml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/pylint.xml b/.idea/pylint.xml new file mode 100644 index 0000000..86e7be6 --- /dev/null +++ b/.idea/pylint.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/runConfigurations/build.xml b/.idea/runConfigurations/build.xml new file mode 100644 index 0000000..b921569 --- /dev/null +++ b/.idea/runConfigurations/build.xml @@ -0,0 +1,25 @@ + + + + + \ No newline at end of file diff --git a/.idea/runConfigurations/lint.xml b/.idea/runConfigurations/lint.xml new file mode 100644 index 0000000..58e3152 --- /dev/null +++ b/.idea/runConfigurations/lint.xml @@ -0,0 +1,25 @@ + + + + + \ No newline at end of file diff --git a/.idea/runConfigurations/pytest_in_tests.xml b/.idea/runConfigurations/pytest_in_tests.xml new file mode 100644 index 0000000..e8629e0 --- /dev/null +++ b/.idea/runConfigurations/pytest_in_tests.xml @@ -0,0 +1,21 @@ + + + + + \ No newline at end of file diff --git a/.idea/runConfigurations/run.xml b/.idea/runConfigurations/run.xml new file mode 100644 index 0000000..028572a --- /dev/null +++ b/.idea/runConfigurations/run.xml @@ -0,0 +1,17 @@ + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..b762af0 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..23353d7 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,128 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, religion, or sexual identity +and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the + overall community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or + advances of any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email + address, without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +imfeldma+02_q@gmail.com. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series +of actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or +permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within +the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.0, available at +https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. + +Community Impact Guidelines were inspired by [Mozilla's code of conduct +enforcement ladder](https://github.com/mozilla/diversity). + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see the FAQ at +https://www.contributor-covenant.org/faq. Translations are available at +https://www.contributor-covenant.org/translations. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..be96b11 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,3 @@ +# Contributing to this repository + +This is reasonably small for you to just fork and raise a PR. Have fun. \ No newline at end of file diff --git a/LICENSE b/LICENSE index f2f1fd6..263a877 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2022 MrMatOrg +Copyright (c) 2021 Mathieu Imfeld Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..7578cfa --- /dev/null +++ b/Makefile @@ -0,0 +1,46 @@ +# +# Convenience Makefile +# Useful reference: https://makefiletutorial.com + +GIT_SHA := $(shell git rev-parse --short HEAD) +VERSION ?= 0.0.0-dev0.${GIT_SHA} +PYTHON_VERSION := $(shell echo "${VERSION}" | sed -e 's/-dev0\./-dev0+/') + +PYTHON_SOURCES := $(shell find src/mrmat_python_api_fastapi -name '*.py') +PYTHON_TARGET := dist/mrmat_python_api_fastapi-${PYTHON_VERSION}-py3-none-any.whl +CONTAINER_SOURCES := $(shell find var/container) +HELM_SOURCES := $(shell find var/helm) +HELM_TARGET := dist/mrmat-python-api-fastapi-$(VERSION).tgz + +all: python container helm +python: $(PYTHON_TARGET) +helm: $(HELM_TARGET) + +$(PYTHON_TARGET): $(PYTHON_SOURCES) + MRMAT_VERSION="${PYTHON_VERSION}" python -mbuild -n --wheel + +$(HELM_TARGET): $(HELM_SOURCES) container + helm package \ + --app-version "$(VERSION)" \ + --version $(VERSION) \ + --destination dist/ \ + var/helm + +container: $(PYTHON_TARGET) $(CONTAINER_SOURCES) + docker build \ + -f var/container/Dockerfile \ + -t localhost:5001/mrmat-python-api-fastapi:$(VERSION) \ + --build-arg MRMAT_VERSION=$(VERSION) \ + . + docker push localhost:5001/mrmat-python-api-fastapi:$(VERSION) + +helm-install: $(HELM_TARGET) + helm upgrade \ + mrmat-python-api-fastapi \ + ${HELM_TARGET} \ + --install \ + --create-namespace \ + --namespace mrmat-python-api-fastapi + +clean: + rm -rf build dist diff --git a/README.md b/README.md index 473dfaa..2e615c5 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,7 @@ -# mrmat-python-api-fastapi +# MrMat :: Python :: API :: FastAPI + +[![Build](https://github.com/MrMatOrg/mrmat-python-api-fastapi/actions/workflows/build.yml/badge.svg)](https://github.com/MrMatOrg/mrmat-python-api-fastapi/actions/workflows/build.yml) + + Boilerplate (and playground) for a code-first Python FastAPI API, with all the bells and whistles we've come to expect + diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..5d4aaf9 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,13 @@ +# Security Policy + +## Supported Versions + +I'd be surprised if you'd want support for this. But be my guest raising an issue... + +| Version | Supported | +|---------|--------------------| +| all | :white_check_mark: | + +## Reporting a Vulnerability + +Raise an issue straight on this repository. diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..a6288cb --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,60 @@ +[build-system] +requires = [ + 'setuptools==76.0.0', + 'wheel==0.45.1' +] +build-backend = 'setuptools.build_meta' + +[project] +name = "mrmat-python-api-fastapi" +description = "A Python API using FastAPI" +urls = { "Sources" = "https://github.com/MrMatAP/mrmat-python-api-fastapi.git" } +keywords = ["api", "python", "fastapi"] +readme = "README.md" +license = { text = "MIT" } +authors = [ + { "name" = "Mathieu Imfeld", "email" = "imfeldma+9jqerw@gmail.com" } +] +maintainers = [ + { "name" = "Mathieu Imfeld", "email" = "imfeldma+9jqerw@gmail.com" } +] +classifiers = [ + "Development Status :: 3 - Alpha", + "License :: OSI Approved :: MIT", + "Programming Language :: Python :: 3.12" +] +requires-python = ">=3.12" +dynamic = ["version", "dependencies", "optional-dependencies"] + +[tool.setuptools.dynamic] +version = { attr = "ci.version"} +dependencies = {file = ["requirements.txt"]} +optional-dependencies = { dev = {file = ["requirements.dev.txt"] } } + +[tool.setuptools.packages.find] +where = ["src"] +include = ["mrmat_python_api_fastapi*"] +namespaces = true + +[tool.setuptools.package-data] +"*" = [".mo", "*.yml", "*.yaml", "*.md", "inventory", "*.j2", "*.html", "*.ico", "*.css", "*.js", "*.svg", "*.woff", "*.eot", "*.ttf"] + +[project.scripts] +mrmat-python-api-fastapi = "mrmat_python_api_fastapi.app:run" + +[tool.mypy] +plugins = [ 'pydantic.mypy' ] + +[tool.pydantic-mypy] +init_forbid_extra = true +init_typed = true +warn_required_dynamic_aliases = true + +[tool.pytest.ini_options] +testpaths = 'tests' +addopts = '--cov=mrmat_python_api_fastapi --cov-report=term --cov-report=xml:build/coverage.xml --junit-xml=build/junit.xml' +junit_family = 'xunit2' +log_cli = 1 +log_cli_level = 'INFO' +log_cli_format = '%(asctime)s [%(levelname)8s] %(message)s (%(filename)s:%(lineno)s)' +log_cli_date_format = '%Y-%m-%d %H:%M:%S' diff --git a/requirements.dev.txt b/requirements.dev.txt new file mode 100644 index 0000000..16d0fe0 --- /dev/null +++ b/requirements.dev.txt @@ -0,0 +1,15 @@ +# +# This file is for convenience only, to quickly set up what is necessary to develop in some IDE +# The real runtime requirements are held in pyproject.toml. BE SURE to update both files if necessary. + +# Build/Test requirements + +setuptools==76.0.0 +build==1.2.2.post1 # MIT +wheel==0.45.1 # MIT + +pytest==8.3.5 # MIT +pytest-cov==6.0.0 # MIT +mypy==1.15.0 # MIT +types-PyYAML==6.0.12.20241230 # Apache 2.0 +httpx==0.28.1 # BSD-3-Clause diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..7e66d3c --- /dev/null +++ b/requirements.txt @@ -0,0 +1,7 @@ +# +# Runtime requirements + +fastapi==0.115.11 # MIT +sqlalchemy[asyncio]==2.0.40 # MIT +uvicorn==0.34.0 # BSD 3-Clause +pydantic==2.10.6 # MIT diff --git a/src/ci/__init__.py b/src/ci/__init__.py new file mode 100644 index 0000000..63ed8b0 --- /dev/null +++ b/src/ci/__init__.py @@ -0,0 +1,30 @@ +# MIT License +# +# Copyright (c) 2022 MrMat +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +""" +Build-time only module to determine the version of this project from CI and if +not provided a reasonable development-time default. +""" + +import os + +version = os.environ.get('MRMAT_VERSION', '0.0.0.dev0') diff --git a/src/mrmat_python_api_fastapi/__init__.py b/src/mrmat_python_api_fastapi/__init__.py new file mode 100644 index 0000000..32dea2a --- /dev/null +++ b/src/mrmat_python_api_fastapi/__init__.py @@ -0,0 +1,33 @@ +# MIT License +# +# Copyright (c) 2022 MrMat +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +from pydantic import BaseModel + +from .config import Config +app_config = Config.from_json_file() +from sqlalchemy.orm import DeclarativeBase + +class ORMBase(DeclarativeBase): + pass + + +class SchemaBase(BaseModel): + model_config = {'from_attributes': True} diff --git a/src/mrmat_python_api_fastapi/apis/__init__.py b/src/mrmat_python_api_fastapi/apis/__init__.py new file mode 100644 index 0000000..ea100a6 --- /dev/null +++ b/src/mrmat_python_api_fastapi/apis/__init__.py @@ -0,0 +1,31 @@ +# MIT License +# +# Copyright (c) 2022 MrMat +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +from pydantic import BaseModel + + +class StatusSchema(BaseModel): + """ + A generic message class + """ + code: int + msg: str diff --git a/src/mrmat_python_api_fastapi/apis/greeting/__init__.py b/src/mrmat_python_api_fastapi/apis/greeting/__init__.py new file mode 100644 index 0000000..5ba3d52 --- /dev/null +++ b/src/mrmat_python_api_fastapi/apis/greeting/__init__.py @@ -0,0 +1,25 @@ +# MIT License +# +# Copyright (c) 2022 MrMat +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +from .v1 import api_greeting_v1 +from .v2 import api_greeting_v2 +from .v3 import api_greeting_v3 diff --git a/src/mrmat_python_api_fastapi/apis/greeting/v1/__init__.py b/src/mrmat_python_api_fastapi/apis/greeting/v1/__init__.py new file mode 100644 index 0000000..bf5e696 --- /dev/null +++ b/src/mrmat_python_api_fastapi/apis/greeting/v1/__init__.py @@ -0,0 +1,24 @@ +# MIT License +# +# Copyright (c) 2022 MrMat +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +from .model import GreetingV1Output # noqa: F401 +from .api import router as api_greeting_v1 # noqa: F401 diff --git a/src/mrmat_python_api_fastapi/apis/greeting/v1/api.py b/src/mrmat_python_api_fastapi/apis/greeting/v1/api.py new file mode 100644 index 0000000..565971d --- /dev/null +++ b/src/mrmat_python_api_fastapi/apis/greeting/v1/api.py @@ -0,0 +1,41 @@ +# MIT License +# +# Copyright (c) 2022 MrMat +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +from fastapi import APIRouter +from mrmat_python_api_fastapi.apis.greeting.v1 import GreetingV1Output + +router = APIRouter() + + +@router.get('/', + response_model=GreetingV1Output, + name='get_greeting', + description='Get a generic greeting since this version of the greeting API does not have a means to ' + 'determine who you are', + response_description='A JSON-encoded Hello World message') +async def get_greeting(): + """ + Return a Hello World message + Returns: + A Hello World message + """ + return GreetingV1Output() diff --git a/src/mrmat_python_api_fastapi/apis/greeting/v1/model.py b/src/mrmat_python_api_fastapi/apis/greeting/v1/model.py new file mode 100644 index 0000000..34c1687 --- /dev/null +++ b/src/mrmat_python_api_fastapi/apis/greeting/v1/model.py @@ -0,0 +1,27 @@ +# MIT License +# +# Copyright (c) 2022 MrMat +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +from pydantic import BaseModel, Field + + +class GreetingV1Output(BaseModel): + message: str = Field(default='Hello World', title='A generic greeting message') diff --git a/src/mrmat_python_api_fastapi/apis/greeting/v2/__init__.py b/src/mrmat_python_api_fastapi/apis/greeting/v2/__init__.py new file mode 100644 index 0000000..561c454 --- /dev/null +++ b/src/mrmat_python_api_fastapi/apis/greeting/v2/__init__.py @@ -0,0 +1,24 @@ +# MIT License +# +# Copyright (c) 2022 MrMat +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +from .model import GreetingV2Output # noqa: F401 +from .api import router as api_greeting_v2 # noqa: F401 diff --git a/src/mrmat_python_api_fastapi/apis/greeting/v2/api.py b/src/mrmat_python_api_fastapi/apis/greeting/v2/api.py new file mode 100644 index 0000000..2f92a2a --- /dev/null +++ b/src/mrmat_python_api_fastapi/apis/greeting/v2/api.py @@ -0,0 +1,40 @@ +# MIT License +# +# Copyright (c) 2022 MrMat +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +""" +Blueprint for the Greeting API in V2 +""" + +from typing import Optional +from fastapi import APIRouter +from mrmat_python_api_fastapi.apis.greeting.v2 import GreetingV2Output + +router = APIRouter() + + +@router.get('/', + response_model=GreetingV2Output, + name='get_greeting_v2', + description='Get a greeting for a given name', + response_description='A JSON-encoded greeting for the provided name') +async def get_greeting(name: Optional[str] = 'Stranger'): + return GreetingV2Output(message=f'Hello {name}') diff --git a/src/mrmat_python_api_fastapi/apis/greeting/v2/model.py b/src/mrmat_python_api_fastapi/apis/greeting/v2/model.py new file mode 100644 index 0000000..d49c247 --- /dev/null +++ b/src/mrmat_python_api_fastapi/apis/greeting/v2/model.py @@ -0,0 +1,27 @@ +# MIT License +# +# Copyright (c) 2022 MrMat +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +from pydantic import BaseModel, Field + + +class GreetingV2Output(BaseModel): + message: str = Field(description='A greeting message') diff --git a/src/mrmat_python_api_fastapi/apis/greeting/v3/__init__.py b/src/mrmat_python_api_fastapi/apis/greeting/v3/__init__.py new file mode 100644 index 0000000..10a0a2c --- /dev/null +++ b/src/mrmat_python_api_fastapi/apis/greeting/v3/__init__.py @@ -0,0 +1,24 @@ +# MIT License +# +# Copyright (c) 2022 MrMat +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +from .model import GreetingV3Output # noqa: F401 +from .api import router as api_greeting_v3 # noqa: F401 diff --git a/src/mrmat_python_api_fastapi/apis/greeting/v3/api.py b/src/mrmat_python_api_fastapi/apis/greeting/v3/api.py new file mode 100644 index 0000000..50a0b89 --- /dev/null +++ b/src/mrmat_python_api_fastapi/apis/greeting/v3/api.py @@ -0,0 +1,35 @@ +# MIT License +# +# Copyright (c) 2022 MrMat +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +from fastapi import APIRouter +from mrmat_python_api_fastapi.apis.greeting.v3 import GreetingV3Output + +router = APIRouter() + + +@router.get('/', + name='get_greeting_v3', + summary='Get a greeting for the authenticated name', + description='This version of the greeting API knows who you are', + response_model=GreetingV3Output) +async def get_greeting(): + return GreetingV3Output(message='Hello foo') diff --git a/src/mrmat_python_api_fastapi/apis/greeting/v3/model.py b/src/mrmat_python_api_fastapi/apis/greeting/v3/model.py new file mode 100644 index 0000000..ab05a30 --- /dev/null +++ b/src/mrmat_python_api_fastapi/apis/greeting/v3/model.py @@ -0,0 +1,31 @@ +# MIT License +# +# Copyright (c) 2022 MrMat +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +""" +Greeting API v3 Model +""" + +from pydantic import BaseModel, Field + + +class GreetingV3Output(BaseModel): + message: str = Field('A greeting message') diff --git a/src/mrmat_python_api_fastapi/apis/healthz/__init__.py b/src/mrmat_python_api_fastapi/apis/healthz/__init__.py new file mode 100644 index 0000000..6ce9ee0 --- /dev/null +++ b/src/mrmat_python_api_fastapi/apis/healthz/__init__.py @@ -0,0 +1,23 @@ +# MIT License +# +# Copyright (c) 2022 MrMat +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +from .api import router as api_healthz # noqa: F401 diff --git a/src/mrmat_python_api_fastapi/apis/healthz/api.py b/src/mrmat_python_api_fastapi/apis/healthz/api.py new file mode 100644 index 0000000..17d10db --- /dev/null +++ b/src/mrmat_python_api_fastapi/apis/healthz/api.py @@ -0,0 +1,60 @@ +# MIT License +# +# Copyright (c) 2022 MrMat +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +""" +Blueprint for the Healthz API +""" + +from fastapi import APIRouter +from pydantic import BaseModel + +router = APIRouter() + +class HealthzSchema(BaseModel): + status: str + +class LivenessSchema(BaseModel): + status: str + +class ReadinessSchema(BaseModel): + status: str + +@router.get('/', + name='Get application health', + description='Get an indication of application health', + response_model=HealthzSchema) +async def healthz() -> HealthzSchema: + return HealthzSchema(status='OK') + +@router.get('/liveness/', + name='Get application liveness', + description='Get an indication of application liveness', + response_model=LivenessSchema) +async def liveness() -> LivenessSchema: + return LivenessSchema(status='OK') + +@router.get('/readiness/', + name='Get application readiness', + description='Get an indication of application readiness', + response_model=ReadinessSchema) +async def readiness() -> ReadinessSchema: + return ReadinessSchema(status='OK') diff --git a/src/mrmat_python_api_fastapi/apis/platform/__init__.py b/src/mrmat_python_api_fastapi/apis/platform/__init__.py new file mode 100644 index 0000000..efc80fa --- /dev/null +++ b/src/mrmat_python_api_fastapi/apis/platform/__init__.py @@ -0,0 +1,23 @@ +# MIT License +# +# Copyright (c) 2022 Mathieu Imfeld +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +from .v1 import api_platform_v1 diff --git a/src/mrmat_python_api_fastapi/apis/platform/v1/__init__.py b/src/mrmat_python_api_fastapi/apis/platform/v1/__init__.py new file mode 100644 index 0000000..6516d40 --- /dev/null +++ b/src/mrmat_python_api_fastapi/apis/platform/v1/__init__.py @@ -0,0 +1,31 @@ +# MIT License +# +# Copyright (c) 2022 Mathieu Imfeld +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +from .api import router as api_platform_v1 +from .schema import ( + ResourceSchema, + ResourceInputSchema, + ResourceListSchema, + OwnerSchema, + OwnerInputSchema, + OwnerListSchema +) \ No newline at end of file diff --git a/src/mrmat_python_api_fastapi/apis/platform/v1/api.py b/src/mrmat_python_api_fastapi/apis/platform/v1/api.py new file mode 100644 index 0000000..3a54c46 --- /dev/null +++ b/src/mrmat_python_api_fastapi/apis/platform/v1/api.py @@ -0,0 +1,287 @@ +# MIT License +# +# Copyright (c) 2022 Mathieu Imfeld +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +from fastapi import APIRouter, Depends, status, HTTPException +from fastapi.responses import Response +from sqlalchemy.exc import SQLAlchemyError +from sqlalchemy.orm import Session + +from mrmat_python_api_fastapi.db import get_db +from mrmat_python_api_fastapi.apis import StatusSchema +from .db import Resource, Owner +from .schema import ( + ResourceSchema, + ResourceInputSchema, + ResourceListSchema, + OwnerSchema, + OwnerInputSchema, + OwnerListSchema +) + +router = APIRouter() + + +@router.get('/resources', + name='list_resources', + summary='List all known resources', + description='Returns all currently known resources and their metadata', + response_model=ResourceListSchema) +async def get_resources(session: Session = Depends(get_db)) -> ResourceListSchema: + try: + resources = session.query(Resource).all() + return ResourceListSchema(resources=[ResourceSchema(uid=r.uid, + name=r.name, + owner_uid=r.owner_uid) + for r in resources]) + except SQLAlchemyError as e: + # Handle the error appropriately, maybe raise an HTTPException + raise HTTPException(status_code=500, detail="A database error occurred") from e + + +@router.get('/resources/{uid}', + name='get_resource', + summary='Get a single resource', + description='Return a single resource identified by its resource id', + responses={ + status.HTTP_404_NOT_FOUND: { + 'description': 'The resource was not found', + 'model': StatusSchema + }, + status.HTTP_200_OK: { + 'description': 'The requested resource', + 'model': ResourceSchema + } + }) +async def get_resource(uid: str, + response: Response, + session: Session = Depends(get_db)): + try: + resource = session.get(Resource, uid) + if not resource: + response.status_code = 404 + return StatusSchema(code=404, msg='The resource was not found') + return resource + except SQLAlchemyError as e: + # Handle the error appropriately, maybe raise an HTTPException + raise HTTPException(status_code=500, detail="A database error occurred") from e + +@router.post('/resources', + name='create_resource', + summary='Create a resource', + description='Create a resource owned by the authenticated user', + response_model=ResourceSchema) +async def create_resource(data: ResourceInputSchema, + response: Response, + session: Session = Depends(get_db)): + try: + resource = Resource(name=data.name, owner_uid=data.owner_uid) + session.add(resource) + session.commit() + response.status_code = 201 + return resource + except SQLAlchemyError as e: + raise HTTPException(status_code=500, detail="A database error occurred") from e + + +@router.put('/resources/{uid}', + name='modify_resource', + summary='Modify a resource', + description='Modify a resource by its resource id', + responses={ + status.HTTP_200_OK: { + 'description': 'The resource was modified', + 'model': ResourceSchema + }, + status.HTTP_404_NOT_FOUND: { + 'description': 'The resource was not found', + 'model': StatusSchema + } + }, + response_model=ResourceSchema) +async def modify_resource(uid: str, + data: ResourceInputSchema, + response: Response, + session: Session = Depends(get_db)): + try: + resource = session.get(Resource, uid) + if not resource: + response.status_code = 404 + return StatusSchema(code=404, msg='Not found') + resource.name = data.name + session.add(resource) + session.commit() + return resource + except SQLAlchemyError as e: + raise HTTPException(status_code=500, detail="A database error occurred") from e + + +@router.delete('/resources/{uid}', + name='remove_resource', + summary='Remove a resource', + description='Remove a resource by its resource id', + status_code=204, + responses={ + status.HTTP_204_NO_CONTENT: { + 'description': 'The resource was removed' + }, + status.HTTP_410_GONE: { + 'description': 'The resource was already gone', + 'model': StatusSchema + } + }) +async def remove_resource(uid: str, + response: Response, + session: Session = Depends(get_db)): + try: + resource = session.get(Resource, uid) + if not resource: + response.status_code = 410 + return StatusSchema(code=410, msg='The resource was already gone') + session.delete(resource) + session.commit() + response.status_code = 204 + return {} + except SQLAlchemyError as e: + raise HTTPException(status_code=500, detail="A database error occurred") from e + + + +@router.get('/owners', + name='list_owners', + summary='List all known owners', + description='Returns all currently known owners and their metadata', + response_model=OwnerListSchema) +async def get_owners(session: Session = Depends(get_db)): + try: + owners = session.query(Owner).all() + return OwnerListSchema(owners=[OwnerSchema(uid=o.uid, name=o.name) for o in owners]) + except SQLAlchemyError as e: + # Handle the error appropriately, maybe raise an HTTPException + raise HTTPException(status_code=500, detail="A database error occurred") from e + + +@router.get('/owners/{uid}', + name='get_owner', + summary='Get a single owner', + description='Return a single owner identified by its owner id', + responses={ + status.HTTP_404_NOT_FOUND: { + 'description': 'The owner was not found', + 'model': StatusSchema + }, + status.HTTP_200_OK: { + 'description': 'The requested owner', + 'model': OwnerSchema + } + }, + response_model=OwnerSchema) +async def get_owner(uid: str, + response: Response, + session: Session = Depends(get_db)): + try: + owner = session.get(Owner, uid) + if not owner: + response.status_code = 404 + return StatusSchema(code=404, msg='The owner was not found') + return owner + except SQLAlchemyError as e: + raise HTTPException(status_code=500, detail="A database error occurred") from e + + +@router.post('/owners', + name='create_owner', + summary='Create a owner', + description='Create a owner', + response_model=OwnerSchema) +async def create_owner(data: OwnerInputSchema, + response: Response, + session: Session = Depends(get_db)): + try: + owner = Owner(name=data.name, client_id='TODO') + session.add(owner) + session.commit() + response.status_code = 201 + return owner + except SQLAlchemyError as e: + # Handle the error appropriately, maybe raise an HTTPException + raise HTTPException(status_code=500, detail="A database error occurred") from e + + +@router.put('/owners/{uid}', + name='modify_owner', + summary='Modify a owner', + description='Modify a owner by its owner id', + responses={ + status.HTTP_200_OK: { + 'description': 'The owner was modified', + 'model': OwnerSchema + }, + status.HTTP_404_NOT_FOUND: { + 'description': 'The owner was not found', + 'model': StatusSchema + } + }, + response_model=OwnerSchema) +async def modify_owner(uid: str, + data: OwnerInputSchema, + response: Response, + session: Session = Depends(get_db)): + try: + owner = session.get(Owner, uid) + if not owner: + response.status_code = 404 + return StatusSchema(code=404, msg='The owner was not found') + owner.name = data.name + session.add(owner) + session.commit() + return owner + except SQLAlchemyError as e: + raise HTTPException(status_code=500, detail="A database error occurred") from e + + +@router.delete('/owners/{uid}', + name='remove_owner', + summary='Remove a owner', + description='Remove a owner by its owner id', + status_code=204, + responses={ + status.HTTP_204_NO_CONTENT: { + 'description': 'The owner was removed' + }, + status.HTTP_410_GONE: { + 'description': 'The owner was already gone', + 'model': StatusSchema + } + }) +async def remove_owner(uid: str, + response: Response, + session: Session = Depends(get_db)): + try: + owner = session.get(Owner, uid) + if not owner: + response.status_code = status.HTTP_410_GONE + return + session.delete(owner) + session.commit() + response.status_code = status.HTTP_204_NO_CONTENT + except SQLAlchemyError as e: + raise HTTPException(status_code=500, detail="A database error occurred") from e diff --git a/src/mrmat_python_api_fastapi/apis/platform/v1/db.py b/src/mrmat_python_api_fastapi/apis/platform/v1/db.py new file mode 100644 index 0000000..fb90a02 --- /dev/null +++ b/src/mrmat_python_api_fastapi/apis/platform/v1/db.py @@ -0,0 +1,48 @@ +# MIT License +# +# Copyright (c) 2022 Mathieu Imfeld +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import uuid +from sqlalchemy import ForeignKey, String, UniqueConstraint, UUID +from sqlalchemy.orm import relationship, Mapped, mapped_column + +from mrmat_python_api_fastapi import ORMBase + + +class Owner(ORMBase): + __tablename__ = 'owners' + __schema__ = 'mrmat-python-api-fastapi' + uid: Mapped[str] = mapped_column(String, primary_key=True, default=str(uuid.uuid4())) + + client_id: Mapped[str] = mapped_column(String(255), nullable=False, unique=True) + name: Mapped[str] = mapped_column(String(255), nullable=False) + resources: Mapped[list["Resource"]] = relationship('Resource', back_populates='owner') + + +class Resource(ORMBase): + __tablename__ = 'resources' + __schema__ = 'mrmat-python-api-fastapi' + uid: Mapped[str] = mapped_column(String, primary_key=True, default=str(uuid.uuid4())) + owner_uid: Mapped[str] = mapped_column(String, ForeignKey('owners.uid'), nullable=False) + name: Mapped[str] = mapped_column(String(255), nullable=False) + + owner: Mapped["Owner"] = relationship('Owner', back_populates='resources') + __table_args__ = (UniqueConstraint('owner_uid', 'name', name='no_duplicate_names_per_owner'),) diff --git a/src/mrmat_python_api_fastapi/apis/platform/v1/schema.py b/src/mrmat_python_api_fastapi/apis/platform/v1/schema.py new file mode 100644 index 0000000..553372c --- /dev/null +++ b/src/mrmat_python_api_fastapi/apis/platform/v1/schema.py @@ -0,0 +1,45 @@ +# MIT License +# +# Copyright (c) 2022 Mathieu Imfeld +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import typing + +from mrmat_python_api_fastapi import SchemaBase + + +class OwnerInputSchema(SchemaBase): + name: str + +class OwnerSchema(OwnerInputSchema): + uid: str + +class OwnerListSchema(SchemaBase): + owners: typing.Sequence[OwnerSchema] + +class ResourceInputSchema(SchemaBase): + name: str + owner_uid: str + +class ResourceSchema(ResourceInputSchema): + uid: str + +class ResourceListSchema(SchemaBase): + resources: typing.Sequence[ResourceSchema] diff --git a/src/mrmat_python_api_fastapi/app.py b/src/mrmat_python_api_fastapi/app.py new file mode 100644 index 0000000..0f00470 --- /dev/null +++ b/src/mrmat_python_api_fastapi/app.py @@ -0,0 +1,49 @@ +# MIT License +# +# Copyright (c) 2022 Mathieu Imfeld +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +from fastapi import FastAPI + +from mrmat_python_api_fastapi.apis.healthz import api_healthz +from mrmat_python_api_fastapi.apis.greeting import api_greeting_v1, api_greeting_v2, api_greeting_v3 +from mrmat_python_api_fastapi.apis.platform import api_platform_v1 + +app = FastAPI(title='MrMat :: Python :: API :: FastAPI') +app.include_router(api_healthz, prefix='/api/healthz', tags=['health']) +app.include_router(api_greeting_v1, prefix='/api/greeting/v1', tags=['greeting']) +app.include_router(api_greeting_v2, prefix='/api/greeting/v2', tags=['greeting']) +app.include_router(api_greeting_v3, prefix='/api/greeting/v3', tags=['greeting']) +app.include_router(api_platform_v1, prefix='/api/platform/v1', tags=['platform']) + + +@app.get('/') +def index(): + return {'Hello': 'World'} + +def run() -> int: + """ + This is the main entry point for the application when running via the CLI wrapper + Returns: + - int: The exit code + """ + import uvicorn + uvicorn.run(app, host='0.0.0.0', port=8000) + return 0 diff --git a/src/mrmat_python_api_fastapi/config.py b/src/mrmat_python_api_fastapi/config.py new file mode 100644 index 0000000..a7b2691 --- /dev/null +++ b/src/mrmat_python_api_fastapi/config.py @@ -0,0 +1,43 @@ +# MIT License +# +# Copyright (c) 2022 Mathieu Imfeld +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import os +import json +import secrets + + +class Config: + """ + A class to deal with application configuration + """ + secret_key: str = secrets.token_urlsafe(16) + db_url: str = 'sqlite://' + + @staticmethod + def from_json_file(file: str | None = os.getenv('APP_CONFIG')): + runtime_config = Config() + if file and os.path.exists(file): + with open(file, 'r', encoding='UTF-8') as c: + file_config = json.load(c) + runtime_config.secret_key = file_config.get_owner('secret_key', secrets.token_urlsafe(16)) + runtime_config.db_url = file_config.get_owner('db_url', 'sqlite://') + return runtime_config diff --git a/src/mrmat_python_api_fastapi/db.py b/src/mrmat_python_api_fastapi/db.py new file mode 100644 index 0000000..4ae9335 --- /dev/null +++ b/src/mrmat_python_api_fastapi/db.py @@ -0,0 +1,39 @@ +# MIT License +# +# Copyright (c) 2022 Mathieu Imfeld +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +from functools import lru_cache + +from sqlalchemy import create_engine +from sqlalchemy.orm import Session, sessionmaker + +from mrmat_python_api_fastapi import app_config, ORMBase + + +@lru_cache +def get_db() -> Session: + if app_config.db_url.startswith('sqlite'): + engine = create_engine(url=app_config.db_url, connect_args={'check_same_thread': False}) + else: + engine = create_engine(url=app_config.db_url) + session_local = sessionmaker(autocommit=False, autoflush=False, bind=engine) + ORMBase.metadata.create_all(bind=engine) + return session_local() diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..786c910 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,50 @@ +# MIT License +# +# Copyright (c) 2025 Mathieu Imfeld +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import pathlib +import pytest +import fastapi.testclient + +from mrmat_python_api_fastapi import app_config, ORMBase +from mrmat_python_api_fastapi.app import app +from mrmat_python_api_fastapi.db import get_db + +@pytest.fixture(scope='session') +def build_path(): + build = pathlib.Path(__file__).parent.parent.resolve() / 'build' + build.mkdir(exist_ok=True) + return build + +@pytest.fixture(scope='session') +def test_db_path(build_path) -> pathlib.Path: + test_db_path = build_path / "test.db" + if test_db_path.exists(): + test_db_path.unlink() + return test_db_path + +@pytest.fixture(scope='session') +def client(test_db_path) -> fastapi.testclient.TestClient: + app_config.db_url = f'sqlite:///{test_db_path}' + session = get_db() + with session.begin(): + ORMBase.metadata.create_all(session.bind) + return fastapi.testclient.TestClient(app) diff --git a/tests/test_greeting.py b/tests/test_greeting.py new file mode 100644 index 0000000..fdc667c --- /dev/null +++ b/tests/test_greeting.py @@ -0,0 +1,43 @@ +# MIT License +# +# Copyright (c) 2025 Mathieu Imfeld +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import pytest +import fastapi.testclient + +from mrmat_python_api_fastapi.apis.greeting.v1 import GreetingV1Output +from mrmat_python_api_fastapi.apis.greeting.v2 import GreetingV2Output + +def test_greeting_v1(client: fastapi.testclient.TestClient): + response = client.get("/api/greeting/v1") + assert response.status_code == 200 + assert GreetingV1Output.model_validate(response.json(), strict=True).message == 'Hello World' + +def test_greeting_v2(client: fastapi.testclient.TestClient): + response = client.get("/api/greeting/v2") + assert response.status_code == 200 + assert GreetingV2Output.model_validate(response.json(), strict=True).message == 'Hello Stranger' + +@pytest.mark.parametrize('name', ['MrMat', 'Chris', 'Mihal', 'Alexandre', 'Jerome']) +def test_greeting_v2_custom(client: fastapi.testclient.TestClient, name: str): + response = client.get("/api/greeting/v2", params=dict(name=name)) + assert response.status_code == 200 + assert GreetingV2Output.model_validate(response.json(), strict=True).message == f'Hello {name}' diff --git a/tests/test_healthz.py b/tests/test_healthz.py new file mode 100644 index 0000000..d8779f9 --- /dev/null +++ b/tests/test_healthz.py @@ -0,0 +1,40 @@ +# MIT License +# +# Copyright (c) 2025 Mathieu Imfeld +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import fastapi.testclient + +from mrmat_python_api_fastapi.apis.healthz.api import HealthzSchema, LivenessSchema, ReadinessSchema + +def test_healthz(client: fastapi.testclient.TestClient): + response = client.get("/api/healthz") + assert response.status_code == 200 + assert HealthzSchema.model_validate(response.json(), strict=True) + +def test_liveness(client: fastapi.testclient.TestClient): + response = client.get("/api/healthz/liveness") + assert response.status_code == 200 + assert LivenessSchema.model_validate(response.json(), strict=True) + +def test_readiness(client: fastapi.testclient.TestClient): + response = client.get("/api/healthz/readiness") + assert response.status_code == 200 + assert ReadinessSchema.model_validate(response.json(), strict=True) diff --git a/tests/test_platform.py b/tests/test_platform.py new file mode 100644 index 0000000..bf5538c --- /dev/null +++ b/tests/test_platform.py @@ -0,0 +1,101 @@ +# MIT License +# +# Copyright (c) 2025 Mathieu Imfeld +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import fastapi.testclient + +from mrmat_python_api_fastapi.apis.platform.v1 import ( + ResourceSchema, + ResourceInputSchema, + ResourceListSchema, + OwnerSchema, + OwnerInputSchema, + OwnerListSchema +) + +def test_platform_v1(client: fastapi.testclient.TestClient): + response = client.get('/api/platform/v1/owners') + assert response.status_code == 200 + assert len(OwnerListSchema.model_validate(response.json(), strict=True).owners) == 0 + + owner = OwnerInputSchema(name='test-owner') + response = client.post('/api/platform/v1/owners', json=owner.model_dump(mode='json')) + assert response.status_code == 201 + owner_created = OwnerSchema.model_validate(response.json(), strict=True) + assert owner_created.uid is not None + + response = client.get(f'/api/platform/v1/owners/{owner_created.uid}') + assert response.status_code == 200 + owner_retrieved = OwnerSchema.model_validate(response.json(), strict=True) + assert owner_created == owner_retrieved + + response = client.get('/api/platform/v1/resources') + assert response.status_code == 200 + assert len(ResourceListSchema.model_validate(response.json(), strict=True).resources) == 0 + + resource = ResourceInputSchema(name='test-resource', owner_uid=owner_created.uid) + response = client.post('/api/platform/v1/resources', json=resource.model_dump(mode='json')) + assert response.status_code == 201 + resource_created = ResourceSchema.model_validate(response.json(), strict=True) + assert resource_created.uid is not None + assert resource_created.owner_uid == owner_created.uid + + response = client.get(f'/api/platform/v1/resources/{resource_created.uid}') + assert response.status_code == 200 + resource_retrieved = ResourceSchema.model_validate(response.json(), strict=True) + assert resource_created == resource_retrieved + + response = client.get('/api/platform/v1/owners') + assert response.status_code == 200 + assert len(OwnerListSchema.model_validate(response.json(), strict=True).owners) == 1 + + response = client.get('/api/platform/v1/resources') + assert response.status_code == 200 + assert len(ResourceListSchema.model_validate(response.json(), strict=True).resources) == 1 + + owner = OwnerInputSchema(name='modified-owner') + response = client.put(f'/api/platform/v1/owners/{owner_created.uid}', json=owner.model_dump(mode='json')) + assert response.status_code == 200 + owner_updated = OwnerSchema.model_validate(response.json(), strict=True) + assert owner_updated.uid == owner_created.uid + assert owner_updated.name == 'modified-owner' + + response = client.get(f'/api/platform/v1/resources/{resource_created.uid}') + assert response.status_code == 200 + resource_retrieved = ResourceSchema.model_validate(response.json(), strict=True) + assert resource_retrieved.owner_uid == owner_updated.uid + + resource = ResourceInputSchema(name='modified-resource', owner_uid=owner_updated.uid) + response = client.put(f'/api/platform/v1/resources/{resource_created.uid}', json=resource.model_dump(mode='json')) + assert response.status_code == 200 + resource_updated = ResourceSchema.model_validate(response.json(), strict=True) + assert resource_updated.uid == resource_created.uid + assert resource_updated.owner_uid == owner_updated.uid + + response = client.delete(f'/api/platform/v1/resources/{resource_created.uid}') + assert response.status_code == 204 + response = client.delete(f'/api/platform/v1/resources/{resource_created.uid}') + assert response.status_code == 410 + + response = client.delete(f'/api/platform/v1/owners/{owner_created.uid}') + assert response.status_code == 204 + response = client.delete(f'/api/platform/v1/owners/{owner_created.uid}') + assert response.status_code == 410 diff --git a/var/container/Dockerfile b/var/container/Dockerfile new file mode 100644 index 0000000..6a64184 --- /dev/null +++ b/var/container/Dockerfile @@ -0,0 +1,17 @@ +FROM python:3.12-alpine AS build +ARG MRMAT_VERSION="0.0.0.dev0" +ADD dist/mrmat_python_api_fastapi-*-py3-none-any.whl / +RUN pip install --user /mrmat_python_api_fastapi-*-py3-none-any.whl + +FROM python:3.12-alpine +ARG MRMAT_VERSION="0.0.0.dev0" +LABEL VERSION=$MRMAT_VERSION +RUN addgroup -g 1000 app && \ + adduser -g 'App User' -u 1000 -G app -D app +COPY --from=build /root/.local /home/app/.local +RUN chown -R 1000:1000 /home/app/.local + +USER app:app +EXPOSE 8000 +#CMD ["/home/app/.local/bin/uvicorn", "--host", "0.0.0.0", "--port", "8000", "mrmat_python_api_fastapi.app:app"] +CMD ["/home/app/.local/bin/mrmat-python-api-fastapi"] diff --git a/var/helm/.helmignore b/var/helm/.helmignore new file mode 100644 index 0000000..0e8a0eb --- /dev/null +++ b/var/helm/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/var/helm/Chart.yaml b/var/helm/Chart.yaml new file mode 100644 index 0000000..d91f08e --- /dev/null +++ b/var/helm/Chart.yaml @@ -0,0 +1,15 @@ +apiVersion: v2 +name: mrmat-python-api-fastapi +description: A Helm chart for MrMat Python API FastAPI +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +# Versions are expected to follow Semantic Versioning (https://semver.org/) +version: "0.0.0" + +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application. Versions are not expected to +# follow Semantic Versioning. They should reflect the version the application is using. +# It is recommended to use it with quotes. +appVersion: "0.0.0.dev0" diff --git a/var/helm/templates/NOTES.txt b/var/helm/templates/NOTES.txt new file mode 100644 index 0000000..f029577 --- /dev/null +++ b/var/helm/templates/NOTES.txt @@ -0,0 +1 @@ +# MrMat :: Python :: API :: FastAPI installed diff --git a/var/helm/templates/deployment.yaml b/var/helm/templates/deployment.yaml new file mode 100644 index 0000000..c20f14c --- /dev/null +++ b/var/helm/templates/deployment.yaml @@ -0,0 +1,56 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: mrmat-python-api-fastapi + labels: + app.kubernetes.io/name: mrmat-python-api-fastapi + app.kubernetes.io/part-of: mrmat-python-api-fastapi +spec: + replicas: 2 + selector: + matchLabels: + app: {{ .Values.pod.name }} + version: {{ .Chart.AppVersion }} + template: + metadata: + labels: + sidecar.istio.io/inject: "true" + app: {{ .Values.pod.name }} + version: {{ .Chart.AppVersion }} + app.kubernetes.io/name: mrmat-python-api-fastapi + app.kubernetes.io/part-of: mrmat-python-api-fastapi + spec: + serviceAccountName: {{ .Values.serviceAccount.name }} + containers: + - name: mrmat-python-api-fastapi + image: {{ .Values.pod.repository }}:{{ .Chart.AppVersion }} + imagePullPolicy: {{ .Values.pod.imagePullPolicy }} + ports: + - name: http + containerPort: {{ .Values.pod.port }} + protocol: TCP + securityContext: + capabilities: + drop: + - ALL + readOnlyRootFilesystem: true + runAsNonRoot: true + runAsUser: 1000 + livenessProbe: + httpGet: + path: /api/healthz/liveness/ + port: {{ .Values.pod.port }} + readinessProbe: + httpGet: + path: /api/healthz/readiness/ + port: {{ .Values.pod.port }} + affinity: + podAntiAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + - topologyKey: "kubernetes.io/hostname" + labelSelector: + matchExpressions: + - key: app + operator: In + values: + - {{ .Values.pod.name }} diff --git a/var/helm/templates/httproute.yaml b/var/helm/templates/httproute.yaml new file mode 100644 index 0000000..7c76989 --- /dev/null +++ b/var/helm/templates/httproute.yaml @@ -0,0 +1,34 @@ +{{- if .Values.httproute.enabled -}} +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: {{ .Values.httproute.name }} + labels: + app: {{ .Values.pod.name }} + version: {{ .Chart.AppVersion }} + app.kubernetes.io/name: {{ .Values.httproute.name }} + app.kubernetes.io/part-of: mrmat-python-api-fastapi +spec: + hostnames: + {{- range .Values.httproute.hostnames }} + - {{ . | quote }} + {{- end }} + - mrmat-python-api-fastapi.local + parentRefs: + {{- range .Values.httproute.parents }} + - group: gateway.networking.k8s.io + kind: Gateway + name: {{ .name }} + namespace: {{ .namespace }} + {{- end }} + rules: + - backendRefs: + - kind: Service + name: {{ .Values.service.name }} + port: {{ .Values.service.port }} + weight: 1 + matches: + - path: + type: PathPrefix + value: / +{{- end -}} diff --git a/var/helm/templates/ingress.yaml b/var/helm/templates/ingress.yaml new file mode 100644 index 0000000..5bdb791 --- /dev/null +++ b/var/helm/templates/ingress.yaml @@ -0,0 +1,43 @@ +{{- if .Values.ingress.enabled -}} +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: {{ include "helm.fullname" . }} + labels: + {{- include "helm.labels" . | nindent 4 }} + {{- with .Values.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- with .Values.ingress.className }} + ingressClassName: {{ . }} + {{- end }} + {{- if .Values.ingress.tls }} + tls: + {{- range .Values.ingress.tls }} + - hosts: + {{- range .hosts }} + - {{ . | quote }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} + {{- end }} + rules: + {{- range .Values.ingress.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ .path }} + {{- with .pathType }} + pathType: {{ . }} + {{- end }} + backend: + service: + name: {{ include "helm.fullname" $ }} + port: + number: {{ $.Values.service.port }} + {{- end }} + {{- end }} +{{- end }} diff --git a/var/helm/templates/service.yaml b/var/helm/templates/service.yaml new file mode 100644 index 0000000..3f38c96 --- /dev/null +++ b/var/helm/templates/service.yaml @@ -0,0 +1,19 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ .Values.service.name }} + labels: + app: {{ .Values.pod.name }} + version: {{ .Chart.AppVersion }} + app.kubernetes.io/name: {{ .Values.service.name }} + app.kubernetes.io/part-of: mrmat-python-api-fastapi +spec: + type: ClusterIP + ports: + - port: {{ .Values.service.port }} + targetPort: {{ .Values.pod.port }} + protocol: TCP + name: http + selector: + app: {{ .Values.pod.name }} + version: {{ .Chart.AppVersion }} diff --git a/var/helm/templates/serviceaccount.yaml b/var/helm/templates/serviceaccount.yaml new file mode 100644 index 0000000..88b1e7a --- /dev/null +++ b/var/helm/templates/serviceaccount.yaml @@ -0,0 +1,10 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ .Values.serviceAccount.name }} + labels: + app: {{ .Values.pod.name }} + version: {{ .Chart.AppVersion }} + app.kubernetes.io/name: {{ .Values.serviceAccount.name }} + app.kubernetes.io/part-of: mrmat-python-api-fastapi +automountServiceAccountToken: true diff --git a/var/helm/values.yaml b/var/helm/values.yaml new file mode 100644 index 0000000..25a7a29 --- /dev/null +++ b/var/helm/values.yaml @@ -0,0 +1,46 @@ +# +# Default values + +# This is to override the chart name. +nameOverride: "" +fullnameOverride: "" + +serviceAccount: + name: mrmat-python-api-fastapi-sa + +pod: + name: mrmat-python-api-fastapi + replicas: 2 + repository: mrmat-registry:5000/mrmat-python-api-fastapi + imagePullPolicy: IfNotPresent + port: 8000 + +service: + name: mrmat-python-api-fastapi-svc + port: 80 + +httproute: + enabled: true + name: mrmat-python-api-fastapi-httproute + hostnames: + - mrmat-python-api-fastapi.local + parents: + - name: istio-ingress + namespace: edge + +ingress: + enabled: false + name: mrmat-python-api-fastapi-ing + className: "" + annotations: { } + # kubernetes.io/ingress.class: nginx + # kubernetes.io/tls-acme: "true" + hosts: + - host: mrmat-python-api-fastapi.local + paths: + - path: / + pathType: ImplementationSpecific + tls: [ ] + # - secretName: chart-example-tls + # hosts: + # - chart-example.local