├── .github └── workflows │ └── main.yml ├── .gitignore ├── LICENSE ├── README.md ├── picoapi ├── __init__.py ├── __main__.py ├── api.py ├── healthcheck.py └── schema.py ├── requirements.txt └── setup.py /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | # Sequence of patterns matched against refs/tags 4 | tags: 5 | - 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10 6 | 7 | name: Publish Release 8 | 9 | jobs: 10 | build: 11 | name: Publish Release 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout code 15 | uses: actions/checkout@v2 16 | 17 | - uses: actions/setup-python@v2 18 | with: 19 | python-version: '3.x' 20 | 21 | - name: Set Version 22 | run: | 23 | echo "RELEASE_VERSION=$(echo ${GITHUB_REF:10})" >> $GITHUB_ENV 24 | 25 | - name: Build Package 26 | run: | 27 | python -m pip install --upgrade pip wheel 28 | python setup.py build sdist bdist_wheel 29 | 30 | - name: Publish package to PyPI 31 | uses: pypa/gh-action-pypi-publish@release/v1 32 | with: 33 | user: __token__ 34 | password: ${{ secrets.PYPI_API_TOKEN }} 35 | 36 | - name: Publish package to Private PyPI 37 | uses: pypa/gh-action-pypi-publish@release/v1 38 | with: 39 | user: ${{ secrets.PRIVATE_PYPI_USERNAME }} 40 | password: ${{ secrets.PRIVATE_PYPI_PASSWORD }} 41 | repository_url: ${{ secrets.PRIVATE_PYPI_URL }} 42 | 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/* 2 | 3 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider 4 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 5 | 6 | # User-specific stuff 7 | .idea/**/workspace.xml 8 | .idea/**/tasks.xml 9 | .idea/**/usage.statistics.xml 10 | .idea/**/dictionaries 11 | .idea/**/shelf 12 | 13 | # Generated files 14 | .idea/**/contentModel.xml 15 | 16 | # Sensitive or high-churn files 17 | .idea/**/dataSources/ 18 | .idea/**/dataSources.ids 19 | .idea/**/dataSources.local.xml 20 | .idea/**/sqlDataSources.xml 21 | .idea/**/dynamic.xml 22 | .idea/**/uiDesigner.xml 23 | .idea/**/dbnavigator.xml 24 | 25 | # Gradle 26 | .idea/**/gradle.xml 27 | .idea/**/libraries 28 | 29 | # Gradle and Maven with auto-import 30 | # When using Gradle or Maven with auto-import, you should exclude module files, 31 | # since they will be recreated, and may cause churn. Uncomment if using 32 | # auto-import. 33 | # .idea/artifacts 34 | # .idea/compiler.xml 35 | # .idea/jarRepositories.xml 36 | # .idea/modules.xml 37 | # .idea/*.iml 38 | # .idea/modules 39 | # *.iml 40 | # *.ipr 41 | 42 | # CMake 43 | cmake-build-*/ 44 | 45 | # Mongo Explorer plugin 46 | .idea/**/mongoSettings.xml 47 | 48 | # File-based project format 49 | *.iws 50 | 51 | # IntelliJ 52 | out/ 53 | 54 | # mpeltonen/sbt-idea plugin 55 | .idea_modules/ 56 | 57 | # JIRA plugin 58 | atlassian-ide-plugin.xml 59 | 60 | # Cursive Clojure plugin 61 | .idea/replstate.xml 62 | 63 | # Crashlytics plugin (for Android Studio and IntelliJ) 64 | com_crashlytics_export_strings.xml 65 | crashlytics.properties 66 | crashlytics-build.properties 67 | fabric.properties 68 | 69 | # Editor-based Rest Client 70 | .idea/httpRequests 71 | 72 | # Android studio 3.1+ serialized cache file 73 | .idea/caches/build_file_checksums.ser 74 | 75 | # Local History for Visual Studio Code 76 | .history/ 77 | 78 | # Byte-compiled / optimized / DLL files 79 | __pycache__/ 80 | *.py[cod] 81 | *$py.class 82 | 83 | # C extensions 84 | *.so 85 | 86 | # Distribution / packaging 87 | .Python 88 | build/ 89 | develop-eggs/ 90 | dist/ 91 | downloads/ 92 | eggs/ 93 | .eggs/ 94 | lib/ 95 | lib64/ 96 | parts/ 97 | sdist/ 98 | var/ 99 | wheels/ 100 | share/python-wheels/ 101 | *.egg-info/ 102 | .installed.cfg 103 | *.egg 104 | MANIFEST 105 | 106 | # PyInstaller 107 | # Usually these files are written by a python script from a template 108 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 109 | *.manifest 110 | *.spec 111 | 112 | # Installer logs 113 | pip-log.txt 114 | pip-delete-this-directory.txt 115 | 116 | # Unit test / coverage reports 117 | htmlcov/ 118 | .tox/ 119 | .nox/ 120 | .coverage 121 | .coverage.* 122 | .cache 123 | nosetests.xml 124 | coverage.xml 125 | *.cover 126 | *.py,cover 127 | .hypothesis/ 128 | .pytest_cache/ 129 | cover/ 130 | 131 | # Translations 132 | *.mo 133 | *.pot 134 | 135 | # Django stuff: 136 | *.log 137 | local_settings.py 138 | db.sqlite3 139 | db.sqlite3-journal 140 | 141 | # Flask stuff: 142 | instance/ 143 | .webassets-cache 144 | 145 | # Scrapy stuff: 146 | .scrapy 147 | 148 | # Sphinx documentation 149 | docs/_build/ 150 | 151 | # PyBuilder 152 | .pybuilder/ 153 | target/ 154 | 155 | # Jupyter Notebook 156 | .ipynb_checkpoints 157 | 158 | # IPython 159 | profile_default/ 160 | ipython_config.py 161 | 162 | # pyenv 163 | # For a library or package, you might want to ignore these files since the code is 164 | # intended to run in multiple environments; otherwise, check them in: 165 | # .python-version 166 | 167 | # pipenv 168 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 169 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 170 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 171 | # install all needed dependencies. 172 | #Pipfile.lock 173 | 174 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 175 | __pypackages__/ 176 | 177 | # Celery stuff 178 | celerybeat-schedule 179 | celerybeat.pid 180 | 181 | # SageMath parsed files 182 | *.sage.py 183 | 184 | # Environments 185 | .env 186 | .venv 187 | env/ 188 | venv/ 189 | ENV/ 190 | env.bak/ 191 | venv.bak/ 192 | 193 | # Spyder project settings 194 | .spyderproject 195 | .spyproject 196 | 197 | # Rope project settings 198 | .ropeproject 199 | 200 | # mkdocs documentation 201 | /site 202 | 203 | # mypy 204 | .mypy_cache/ 205 | .dmypy.json 206 | dmypy.json 207 | 208 | # Pyre type checker 209 | .pyre/ 210 | 211 | # pytype static type analyzer 212 | .pytype/ 213 | 214 | # Cython debug symbols 215 | cython_debug/ 216 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2021, Patrick Coffey 2 | 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 6 | 7 | Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 8 | 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. 9 | 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | PicoAPI Library 2 | =============== 3 | 4 | The API Logic Library known as PicoAPI is sole API Logic library. It essentially reimplements the FastAPI interface from the Python FastAPI library with addition of some functions and logic that add some benefits for using FastAPI in a microservices architecture. The PicoAPI class itself allows for two distinct modes of operation, the supervisor (traditionally referred to as master) or worker (traditionally referred to as slave) configuration. Worker Microservices perform a task, they are told what to do and when to do it by a supervisor Microservice. The supervisor microservice knows which workers it can distribute tasks to because each worker microservice registers itself with a supervisor microservice on start. 5 | 6 | Registration of worker microservices describes the situation where upon start, the worker microservice sends a put request to a supervisor describing the following about itself: 7 | - Which IP microservice resides on. 8 | - Which port the microservice is bound to. 9 | - The worker microservices version. 10 | - The tags associated with this microservice, these allow the supervisor to know what types of work this microservice can perform and what forms of that work this microservice supports. 11 | - The healthcheck information. 12 | 13 | The concept of a healthcheck is simple, it describes the supervisor asking the worker if it is still running. For the supervisor to perform these checks, the worker microservices provide information about how and when to health check them. A healthcheck definition contains two items, the address to check, and the interval for this check to repeat. 14 | 15 | A PicoAPI supervisor adds the following functionality to the base FastAPI class: 16 | - Endpoints for the worker microservices to register. 17 | - Logic to perform a healthcheck. 18 | 19 | A PicoAPI worker adds the following functionality to the base FastAPI class: 20 | - Logic to register with a supervisor upon start. 21 | - An endpoint to return a healthcheck when the supervisor asks. 22 | 23 | 24 | Usage 25 | ===== 26 | 27 | create a .env file or export the following variables: 28 | 29 | | ENV variable | Required (default) | Default | Description | Examples | Implemented | 30 | |------------------------ |-------------------- |--------------------- |-------------------------------------------------------------------------------------------- |------------------------------------------------ |------------- | 31 | | API_HOST | Yes | | The host of the API | myhost\|123.123.123.123\|localhost\|127.0.0.1 | | 32 | | API_PORT | No | 8888 | The port of the API | 8080 | | 33 | | API_BIND | Yes | | The interface descriptor to bind to | 127.0.0.1\|0.0.0.0 | | 34 | | API_TITLE | Yes | | The FastAPI title | Example API Name | | 35 | | API_DESCRIPTION | No | A brief Description | The FastAPI description | An Example of a short description about an API | | 36 | | API_REGISTER_PATH | Yes | | The url of the Keeper registration endpoint (similar to consul) | http://keeper:8100/register | | 37 | | API_HEALTH_PATH | No | | The path relative to this FastAPI to call for health checks | /health | | 38 | | API_HEALTH_INTERVAL | No | 300 | The frequency to perform health checks (in seconds) | 300 | | 39 | | API_VERSION | No | 0.0.1 | The version of this FastAPI | 0.0.1-alpha | | 40 | | API_TAGS | No | | The tags for this microservice, used as part of discovery, delimited with ":" (like $PATH) | servicetag1:servicetag2:servicetag3 | | 41 | | API_CORS_ALLOW_ORIGINS | No | * | The CORS allowed origins, delimited with "!" | | No | 42 | | API_CORS_ALLOW_METHODS | No | * | The CORS allowed methods, delimited with "!" | | No | 43 | | API_CORS_ALLOW_HEADERS | No | * | The CORS allowed headers, delimited with "!" | | No | 44 | 45 | Example .env file: 46 | 47 | ```bash 48 | # API config 49 | # ========== 50 | API_BIND="0.0.0.0" 51 | API_HOST="localhost" 52 | API_PORT="8888" 53 | API_TITLE="test" 54 | API_DESCRIPTION="test description" 55 | API_VERSION="0.0.1-alpha" 56 | 57 | # microservice registration 58 | # ========================= 59 | API_KEEPER_URL="http://localhost:8100/register" 60 | API_HEALTH_PATH="/health" 61 | API_HEALTH_INTERVAL="300" 62 | ``` 63 | 64 | Authors & Contributors 65 | ====================== 66 | 67 | - [Patrick Coffey](https://github.com/schlerp) - Author 68 | - [Asanga Abeyaratne](https://github.com/asaabey) - Contributor 69 | -------------------------------------------------------------------------------- /picoapi/__init__.py: -------------------------------------------------------------------------------- 1 | from .api import PicoAPI -------------------------------------------------------------------------------- /picoapi/__main__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/schlerp/picoapi/27ca04747eab89ba6efefab1c6bf8bdab062deb6/picoapi/__main__.py -------------------------------------------------------------------------------- /picoapi/api.py: -------------------------------------------------------------------------------- 1 | import os 2 | from typing import List 3 | 4 | import requests 5 | from fastapi import FastAPI 6 | from fastapi.responses import JSONResponse 7 | from fastapi.middleware.cors import CORSMiddleware 8 | 9 | import picoapi.schema 10 | import picoapi.healthcheck 11 | 12 | 13 | class NotMasterAPIException(Exception): 14 | def __init__(self, msg="This API is not setup in master mode!"): 15 | self.msg = msg 16 | 17 | 18 | def register_uservice(): 19 | uservice_definition = { 20 | "name": os.getenv("API_TITLE"), 21 | "tags": os.getenv("API_TAGS", "").split(":"), 22 | "host": os.getenv("API_HOST"), 23 | "port": os.getenv("API_PORT"), 24 | "healthcheck": { 25 | "url": "http://{}:{}{}".format( 26 | os.getenv("API_HOST"), 27 | os.getenv("API_PORT"), 28 | os.getenv("API_HEALTH_PATH"), 29 | ), 30 | "interval": os.getenv("API_HEALTH_INTERVAL"), 31 | }, 32 | } 33 | 34 | requests.put(os.getenv("API_REGISTER_PATH"), json=uservice_definition) 35 | 36 | 37 | async def healthcheck(): 38 | return JSONResponse({"status": os.getenv("API_HEALTH_RESPONSE", "I am running!")}) 39 | 40 | 41 | class PicoAPI(FastAPI): 42 | def __init__( 43 | self, 44 | is_master=os.getenv("API_MASTER", False), 45 | api_health_path=os.getenv("API_HEALTH_PATH"), 46 | allow_credentials=True, 47 | allow_origins: List[str] = [ 48 | x for x in os.getenv("API_CORS_ALLOW_ORIGINS", "*").split() 49 | ], 50 | allow_methods: List[str] = [ 51 | x for x in os.getenv("API_CORS_ALLOW_METHODS", "*").split() 52 | ], 53 | allow_headers: List[str] = [ 54 | x for x in os.getenv("API_CORS_ALLOW_HEADERS", "*").split() 55 | ], 56 | *args, 57 | **kwargs 58 | ) -> None: 59 | 60 | # call super class __init__ 61 | super().__init__(*args, **kwargs) 62 | 63 | # add the cors middleware 64 | self.add_middleware( 65 | CORSMiddleware, 66 | allow_origins=allow_origins, 67 | allow_credentials=allow_credentials, 68 | allow_methods=allow_methods, 69 | allow_headers=allow_headers, 70 | ) 71 | 72 | self.is_master = is_master 73 | self.services = [] 74 | self.healthchecks = {} 75 | self.add_api_route(api_health_path, healthcheck) 76 | 77 | if self.is_master: 78 | # add service registration 79 | self.add_api_route("/register", self.add_service) 80 | self.add_api_route("/services/status", self.get_services_status) 81 | self.add_api_route("/services/definition", self.get_services_status) 82 | 83 | else: 84 | # add the service registration event 85 | self.router.on_event("startup", register_uservice) 86 | 87 | async def get_services_status(self): 88 | return JSONResponse( 89 | [ 90 | { 91 | "name": service.name, 92 | "status": self.healthchecks[service.name].get_health() 93 | if self.healthchecks.get(service.name, False) 94 | else "No healthcheck!", 95 | } 96 | for service in self.services 97 | ] 98 | ) 99 | 100 | async def add_service(self, uservice_def: picoapi.schema.MicroserviceDefinition): 101 | self.services.append(uservice_def) 102 | if uservice_def.healthcheck: 103 | self.healthchecks[uservice_def.name] = picoapi.healthcheck.HealthCheck( 104 | uservice_def.healthcheck.url, 105 | uservice_def.healthcheck.interval, 106 | ) 107 | self.healthchecks[uservice_def.name].start() 108 | 109 | async def get_services_openapi(self): 110 | def try_get_json(url): 111 | try: 112 | return requests.get(url).json() 113 | except: 114 | return {"status": "not running!"} 115 | 116 | return JSONResponse( 117 | [ 118 | { 119 | service.name: try_get_json( 120 | "http://{}:{}/openapi.json".format(service.host, service.port) 121 | ), 122 | } 123 | for service in self.services 124 | ] 125 | ) -------------------------------------------------------------------------------- /picoapi/healthcheck.py: -------------------------------------------------------------------------------- 1 | from threading import Thread 2 | import requests 3 | import time 4 | 5 | 6 | class HealthCheck(Thread): 7 | def __init__(self, url: str, interval: int = 30): 8 | super().__init__() 9 | self.url = url 10 | self.interval = interval 11 | self._is_running = True 12 | self.healthy = True 13 | 14 | def get_health(self): 15 | return self.healthy 16 | 17 | def run(self): 18 | while self._is_running: 19 | try: 20 | resp = requests.get(self.url, timeout=3) 21 | status_code = resp.status_code 22 | except: 23 | status_code = 400 24 | self.healthy = ( 25 | "healthy" if (status_code >= 200 and status_code < 300) else "unhealthy" 26 | ) 27 | time.sleep(self.interval) 28 | 29 | def stop(self): 30 | self._is_running = False -------------------------------------------------------------------------------- /picoapi/schema.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, List, Optional 2 | from pydantic import BaseModel 3 | 4 | 5 | class HealthCheckDefinition(BaseModel): 6 | url: str 7 | interval: Optional[int] = 30 8 | 9 | 10 | class MicroserviceDefinition(BaseModel): 11 | name: str 12 | tags: List[str] 13 | host: str 14 | port: int 15 | metadata: Optional[Dict] 16 | healthcheck: Optional[HealthCheckDefinition] 17 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pydantic 2 | fastapi 3 | requests 4 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | import os 3 | 4 | with open("README.md", "r", encoding="utf-8") as f: 5 | long_description = f.read() 6 | 7 | setuptools.setup( 8 | name="picoapi", 9 | version=os.getenv("RELEASE_VERSION", "0.0.1"), 10 | author="Patrick Coffey", 11 | author_email="patrickcoffey91@gmail.com", 12 | description="An opinionated wrapper around FastAPI with custom microservice registration", 13 | long_description=long_description, 14 | long_description_content_type="text/markdown", 15 | url="https://github.com/schlerp/picoapi", 16 | packages=setuptools.find_packages(), 17 | classifiers=[ 18 | "Programming Language :: Python :: 3", 19 | "License :: OSI Approved :: BSD License", 20 | "Operating System :: OS Independent", 21 | ], 22 | install_requires=[ 23 | "pydantic", 24 | "fastapi", 25 | "requests", 26 | ], 27 | python_requires=">=3.6", 28 | ) 29 | --------------------------------------------------------------------------------