├── .coveragerc ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ └── python-package.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .travis.yml ├── AUTHORS ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── MANIFEST.in ├── README.md ├── docs └── index.md ├── examples ├── __init__.py ├── microservice_configuration │ ├── __init__.py │ ├── config.yml │ ├── main.py │ ├── swagger.yaml │ └── views.py ├── microservice_crypt_aws_kms │ ├── __init__.py │ ├── config.yml │ └── main.py ├── microservice_distribued_tracing │ ├── ms1 │ │ ├── config.yml │ │ └── main.py │ └── ms2 │ │ ├── config.yml │ │ └── main.py ├── microservice_metrics │ ├── __init__.py │ ├── config.yml │ ├── main.py │ ├── swagger.yaml │ └── views.py ├── microservice_requests │ ├── __init__.py │ ├── config.yml │ ├── main.py │ ├── swagger.yaml │ └── views.py ├── microservice_service_discovery │ ├── __init__.py │ ├── config.yml │ ├── main.py │ └── service.py ├── microservice_service_discovery_consul │ ├── config.yml │ └── main.py ├── microservice_swagger │ ├── __init__.py │ ├── config.yml │ ├── main.py │ ├── swagger.yaml │ └── views.py ├── microservice_tracer │ ├── __init__.py │ ├── config.yml │ ├── main.py │ ├── swagger.yaml │ └── views.py ├── mininum_microservice │ ├── __init__.py │ ├── config.yml │ └── main.py └── mininum_microservice_docker │ ├── Dockerfile │ ├── __init__.py │ ├── config-docker.yml │ ├── config.yml │ └── main.py ├── mkdocs.yml ├── poetry.lock ├── pylintrc ├── pyms ├── __init__.py ├── cloud │ └── aws │ │ └── kms.py ├── cmd │ ├── __init__.py │ └── main.py ├── config │ ├── __init__.py │ ├── conf.py │ ├── confile.py │ └── resource.py ├── constants.py ├── crypt │ ├── __init__.py │ ├── driver.py │ └── fernet.py ├── exceptions.py ├── flask │ ├── __init__.py │ ├── app │ │ ├── __init__.py │ │ ├── create_app.py │ │ ├── create_config.py │ │ └── utils.py │ ├── configreload │ │ ├── __init__.py │ │ └── configreload.py │ ├── healthcheck │ │ ├── __init__.py │ │ └── healthcheck.py │ └── services │ │ ├── __init__.py │ │ ├── driver.py │ │ ├── metrics.py │ │ ├── requests.py │ │ ├── service_discovery.py │ │ ├── swagger.py │ │ └── tracer.py ├── logger │ ├── __init__.py │ └── logger.py └── utils │ ├── __init__.py │ ├── files.py │ └── utils.py ├── pyproject.toml ├── setup.cfg ├── setup.py ├── tests ├── __init__.py ├── common.py ├── config-tests-bad-structure.yml ├── config-tests-bad-structure2.yml ├── config-tests-bad-structure3.yml ├── config-tests-cache.yml ├── config-tests-cache2.yml ├── config-tests-debug-off.yml ├── config-tests-debug.yml ├── config-tests-deprecated.yml ├── config-tests-encrypted-aws-kms.yml ├── config-tests-encrypted.yml ├── config-tests-flask-encrypted-aws.yml ├── config-tests-flask-encrypted-fernet.yml ├── config-tests-flask-encrypted-none.yml ├── config-tests-flask-swagger.yml ├── config-tests-flask-trace-lightstep.yml ├── config-tests-flask.yml ├── config-tests-metrics.yml ├── config-tests-reload1.yml ├── config-tests-reload2.yml ├── config-tests-requests-no-data.yml ├── config-tests-requests.yml ├── config-tests-service-discovery-consul.yml ├── config-tests-service-discovery.yml ├── config-tests-swagger.yml ├── config-tests-swagger_3.yml ├── config-tests-swagger_3_no_abs_path.yml ├── config-tests-swagger_no_abs_path.yml ├── config-tests.json ├── config-tests.yml ├── conftest.py ├── swagger.yaml ├── swagger3.yaml ├── swagger_for_tests │ ├── info.yaml │ └── swagger.yaml ├── test_cmd.py ├── test_config.py ├── test_crypt.py ├── test_flask.py ├── test_metrics.py ├── test_requests.py ├── test_service_discovery.py ├── test_swagger.py └── test_utils.py └── tox.ini /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | include= 3 | *pyms/* 4 | *tests/* 5 | omit = 6 | *examples/* 7 | .tox/* 8 | venv/* -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Fixes # [refer the issue number] 2 | 3 | Changes proposed in this PR: 4 | - [Updated X functionality] 5 | - [Refactor X class] 6 | -------------------------------------------------------------------------------- /.github/workflows/python-package.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a variety of Python versions 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions 3 | 4 | name: Python package 5 | 6 | on: 7 | push: 8 | branches: [ master ] 9 | pull_request: 10 | branches: [ master ] 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | strategy: 16 | matrix: 17 | python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] 18 | steps: 19 | - uses: actions/checkout@v2 20 | - name: Set up Python ${{ matrix.python-version }} 21 | uses: actions/setup-python@v2 22 | with: 23 | python-version: ${{ matrix.python-version }} 24 | - name: Install dependencies 25 | run: | 26 | pip install --upgrade pip 27 | pip install poetry 28 | poetry install --extras "all" --no-interaction --no-root 29 | env: 30 | POETRY_VIRTUALENVS_CREATE: false 31 | - name: Test with pytest 32 | run: | 33 | pytest --cov=pyms --cov=tests tests/ 34 | - name: Run mypy 35 | continue-on-error: true 36 | run: | 37 | mypy pyms 38 | - name: Lint with flake8 39 | run: | 40 | # stop the build if there are Python syntax errors or undefined names 41 | flake8 pyms --show-source --statistics 42 | - name: Lint with pylint 43 | run: | 44 | pylint --rcfile=pylintrc pyms 45 | # - name: Security safety 46 | # run: | 47 | # safety check 48 | - name: Security bandit 49 | run: | 50 | bandit -r pyms/ -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | venv 2 | .python-version 3 | .idea 4 | *__pycache__* 5 | *.pyc 6 | tmp 7 | *.sqlite3 8 | 9 | # Test&converage 10 | htmlcov/ 11 | coverage.xml 12 | nosetests.xml 13 | .coverage 14 | .tox 15 | py_ms.egg-info/* 16 | .eggs/* 17 | pylintReport.txt 18 | .scannerwork/ 19 | .mypy_cache 20 | 21 | # Deploy 22 | build/ 23 | dist/ 24 | 25 | # other 26 | site/* -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/ambv/black 3 | rev: stable 4 | hooks: 5 | - id: black 6 | files: . 7 | 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "3.8" 4 | install: 5 | - python -m pip install --upgrade pip 6 | - pip install --upgrade setuptools coveralls pipenv 7 | - pipenv lock --dev --requirements > requirements.txt 8 | - pip install -r requirements.txt 9 | script: 10 | - coverage erase 11 | - python setup.py test 12 | after_success: 13 | - coverage combine 14 | - coveralls 15 | 16 | notifications: 17 | email: 18 | - a.vara.1986@gmail.com -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Alberto Vara 2 | Àlex Pérez 3 | Hugo Camino 4 | Javier Luna molina 5 | José Manuel 6 | Mike Rubin -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at a.vara.1986@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | See this [webpage](https://python-microservices.github.io/contributing/) 4 | 5 | ## Documentation 6 | 7 | This project use MkDocs 8 | 9 | * `mkdocs new [dir-name]` - Create a new project. 10 | * `mkdocs serve` - Start the live-reloading docs server. 11 | * `mkdocs build` - Build the documentation site. 12 | * `mkdocs help` - Print this help message. 13 | 14 | ### Project layout 15 | 16 | mkdocs.yml # The configuration file. 17 | docs/ 18 | index.md # The documentation homepage. 19 | ... # Other markdown pages, images and other files. -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | include requirements.txt 3 | include requirements-tests.txt 4 | recursive-include pyms * 5 | recursive-exclude tests * 6 | recursive-exclude examples * 7 | recursive-exclude docker * 8 | prune tests 9 | prune examples -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Python Microservices Library 2 | 3 | [![PyPI version](https://badge.fury.io/py/py-ms.svg)](https://badge.fury.io/py/py-ms) 4 | [![Build Status](https://travis-ci.org/python-microservices/pyms.svg?branch=master)](https://travis-ci.org/python-microservices/pyms) 5 | [![Coverage Status](https://coveralls.io/repos/github/python-microservices/pyms/badge.svg?branch=master)](https://coveralls.io/github/python-microservices/pyms?branch=master) 6 | [![Requirements Status](https://requires.io/github/python-microservices/pyms/requirements.svg?branch=master)](https://requires.io/github/python-microservices/pyms/requirements/?branch=master) 7 | [![Total alerts](https://img.shields.io/lgtm/alerts/g/python-microservices/pyms.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/python-microservices/pyms/alerts/) 8 | [![Language grade: Python](https://img.shields.io/lgtm/grade/python/g/python-microservices/pyms.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/python-microservices/pyms/context:python) 9 | [![Documentation Status](https://readthedocs.org/projects/py-ms/badge/?version=latest)](https://python-microservices.github.io/home/) 10 | [![Gitter](https://img.shields.io/gitter/room/DAVFoundation/DAV-Contributors.svg)](https://gitter.im/python-microservices/pyms) 11 | 12 | PyMS, Python MicroService, is a [Microservice chassis pattern](https://microservices.io/patterns/microservice-chassis.html) 13 | like Spring Boot (Java) or Gizmo (Golang). PyMS is a collection of libraries, best practices and recommended ways to build 14 | microservices with Python which handles cross-cutting concerns: 15 | 16 | - Externalized configuration 17 | - Logging 18 | - Health checks 19 | - Metrics 20 | - Distributed tracing 21 | 22 | PyMS is powered by [Flask](https://flask.palletsprojects.com/en/1.1.x/), [Connexion](https://github.com/spec-first/connexion) 23 | and [Opentracing](https://opentracing.io/). 24 | 25 | Get started with [Installation](https://python-microservices.github.io/installation/) 26 | and then get an overview with the [Quickstart](https://python-microservices.github.io/quickstart/). 27 | 28 | ## Documentation 29 | 30 | To know how to use, install or build a project see the [docs](https://python-microservices.github.io/). 31 | 32 | ## Installation 33 | 34 | ```bash 35 | pip install py-ms[all] 36 | ``` 37 | 38 | ## Quickstart 39 | 40 | See our [quickstart webpage](https://python-microservices.github.io/quickstart/) 41 | 42 | ## Create a project from scaffold 43 | 44 | See our [Create a project from scaffold webpage](https://python-microservices.github.io/quickstart/#create-a-project-from-scaffold) 45 | 46 | ## How To contribute 47 | 48 | We appreciate opening issues and pull requests to make PyMS even more stable & useful! See [This doc](https://python-microservices.github.io/contributing/) 49 | for more details. 50 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # PyMS 2 | 3 | Documentation moved to [https://python-microservices.github.io/](https://python-microservices.github.io/) -------------------------------------------------------------------------------- /examples/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-microservices/pyms/e7c50a127ffe51f67808600a55fddcde7d24664e/examples/__init__.py -------------------------------------------------------------------------------- /examples/microservice_configuration/__init__.py: -------------------------------------------------------------------------------- 1 | from pyms.flask.app import Microservice 2 | 3 | ms = Microservice() 4 | -------------------------------------------------------------------------------- /examples/microservice_configuration/config.yml: -------------------------------------------------------------------------------- 1 | pyms: 2 | services: 3 | requests: 4 | data: "" 5 | swagger: 6 | path: "" 7 | file: "swagger.yaml" 8 | config: 9 | DEBUG: true 10 | TESTING: false 11 | APP_NAME: "Python Microservice" 12 | APPLICATION_ROOT: "" 13 | request_variable_test: "this is a test" 14 | MyVar: "this is MyVar" 15 | test1: "ttest1" 16 | test2: "ttest2" -------------------------------------------------------------------------------- /examples/microservice_configuration/main.py: -------------------------------------------------------------------------------- 1 | from examples.microservice_configuration import ms 2 | 3 | app = ms.create_app() 4 | 5 | if __name__ == "__main__": 6 | app.run() 7 | -------------------------------------------------------------------------------- /examples/microservice_configuration/swagger.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | swagger: "2.0" 3 | info: 4 | description: "This is a sample server Test server" 5 | version: "1.0.0" 6 | title: "Swagger Test list" 7 | termsOfService: "http://swagger.io/terms/" 8 | contact: 9 | email: "apiteam@swagger.io" 10 | license: 11 | name: "Apache 2.0" 12 | url: "http://www.apache.org/licenses/LICENSE-2.0.html" 13 | tags: 14 | - name: "colors" 15 | description: "Everything about your colors" 16 | externalDocs: 17 | description: "Find out more" 18 | url: "http://swagger.io" 19 | - name: "store" 20 | description: "Example endpoint list of colors" 21 | - name: "user" 22 | description: "Operations about user" 23 | externalDocs: 24 | description: "Find out more about our store" 25 | url: "http://swagger.io" 26 | schemes: 27 | - "http" 28 | paths: 29 | /: 30 | get: 31 | tags: 32 | - "test" 33 | summary: "Example endpoint" 34 | description: "" 35 | operationId: "examples.microservice_configuration.views.example" 36 | consumes: 37 | - "application/json" 38 | produces: 39 | - "application/json" 40 | responses: 41 | "200": 42 | description: "A list of colors (may be filtered by palette)" 43 | schema: 44 | $ref: '#/definitions/Example' 45 | "405": 46 | description: "Invalid input" 47 | definitions: 48 | Example: 49 | type: "object" 50 | properties: 51 | main: 52 | type: "string" 53 | externalDocs: 54 | description: "Find out more about Swagger" 55 | url: "http://swagger.io" -------------------------------------------------------------------------------- /examples/microservice_configuration/views.py: -------------------------------------------------------------------------------- 1 | from pyms.flask.app import config 2 | 3 | GLOBAL_VARIABLE = config().request_variable_test 4 | GLOBAL_VARIABLE2 = config().MyVar 5 | 6 | 7 | def example(): 8 | return { 9 | "GLOBAL_VARIABLE": GLOBAL_VARIABLE, 10 | "GLOBAL_VARIABLE2": GLOBAL_VARIABLE2, 11 | "test1": config().test1, 12 | "test2": config().test2, 13 | } 14 | -------------------------------------------------------------------------------- /examples/microservice_crypt_aws_kms/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-microservices/pyms/e7c50a127ffe51f67808600a55fddcde7d24664e/examples/microservice_crypt_aws_kms/__init__.py -------------------------------------------------------------------------------- /examples/microservice_crypt_aws_kms/config.yml: -------------------------------------------------------------------------------- 1 | pyms: 2 | crypt: 3 | method: "aws_kms" 4 | key_id: "alias/prueba-avara" 5 | config: 6 | DEBUG: true 7 | TESTING: false 8 | SWAGGER: true 9 | APP_NAME: business-glossary 10 | APPLICATION_ROOT : "" 11 | SECRET_KEY: "gjr39dkjn344_!67#" 12 | enc_encrypted_key: "AQICAHiALhLQv4eW8jqUccFSnkyDkBAWLAm97Lr2qmdItkUCIAEVoPzSHLW+If9sxSRJ420jAAAAoDCBnQYJKoZIhvcNAQcGoIGPMIGMAgEAMIGGBgkqhkiG9w0BBwEwHgYJYIZIAWUDBAEuMBEEDHoNko2L0A0m/r/h9QIBEIBZPsxFUeHFQzEacdLde5eeJRTHw8e0eSwG7UkJzc+ZdBp1xS9DyqBsHQw4Xnx58iQxCgH6ivRKOraZGKX5ebIZUrw/d+XD8YmbdCosx/TwnHVLneehSbWjF1c=" -------------------------------------------------------------------------------- /examples/microservice_crypt_aws_kms/main.py: -------------------------------------------------------------------------------- 1 | from flask import jsonify 2 | 3 | from pyms.flask.app import Microservice 4 | 5 | ms = Microservice() 6 | app = ms.create_app() 7 | 8 | 9 | @app.route("/") 10 | def example(): 11 | return jsonify({"main": app.ms.config.encrypted_key}) 12 | 13 | 14 | if __name__ == "__main__": 15 | app.run() 16 | -------------------------------------------------------------------------------- /examples/microservice_distribued_tracing/ms1/config.yml: -------------------------------------------------------------------------------- 1 | pyms: 2 | services: 3 | requests: 4 | propagate_headers: true 5 | tracer: 6 | client: "jaeger" 7 | host: "localhost" 8 | component_name: "Python Microservice" 9 | config: 10 | APP_NAME: "Python Microservice" 11 | debug: true 12 | -------------------------------------------------------------------------------- /examples/microservice_distribued_tracing/ms1/main.py: -------------------------------------------------------------------------------- 1 | from flask import current_app, jsonify, request 2 | 3 | from pyms.flask.app import Microservice 4 | 5 | ms = Microservice() 6 | app = ms.create_app() 7 | 8 | 9 | @app.route("/") 10 | def index(): 11 | app.logger.info("There are my headers: \n{}".format(request.headers)) 12 | return jsonify({"main": "hello world {}".format(current_app.config["APP_NAME"])}) 13 | 14 | 15 | if __name__ == "__main__": 16 | app.run() 17 | -------------------------------------------------------------------------------- /examples/microservice_distribued_tracing/ms2/config.yml: -------------------------------------------------------------------------------- 1 | pyms: 2 | services: 3 | requests: 4 | propagate_headers: true 5 | tracer: 6 | client: "jaeger" 7 | host: "localhost" 8 | component_name: "Python Microservice2" 9 | config: 10 | APP_NAME: "Python Microservice2" 11 | debug: true 12 | -------------------------------------------------------------------------------- /examples/microservice_distribued_tracing/ms2/main.py: -------------------------------------------------------------------------------- 1 | from flask import jsonify, request 2 | 3 | from pyms.flask.app import Microservice 4 | 5 | ms = Microservice() 6 | app = ms.create_app() 7 | 8 | 9 | @app.route("/") 10 | def index(): 11 | app.logger.info("There are my headers: \n{}".format(request.headers)) 12 | response = app.ms.requests.get("http://localhost:5000/") 13 | return jsonify({"response": response.json()}) 14 | 15 | 16 | if __name__ == "__main__": 17 | app.run(port=5001) 18 | -------------------------------------------------------------------------------- /examples/microservice_metrics/__init__.py: -------------------------------------------------------------------------------- 1 | from pyms.flask.app import Microservice 2 | 3 | ms = Microservice() 4 | -------------------------------------------------------------------------------- /examples/microservice_metrics/config.yml: -------------------------------------------------------------------------------- 1 | pyms: 2 | services: 3 | metrics: true 4 | requests: 5 | data: "" 6 | swagger: 7 | path: "" 8 | file: "swagger.yaml" 9 | config: 10 | DEBUG: true 11 | TESTING: false 12 | APP_NAME: "Python Microservice" 13 | APPLICATION_ROOT: "" -------------------------------------------------------------------------------- /examples/microservice_metrics/main.py: -------------------------------------------------------------------------------- 1 | from examples.microservice_metrics import ms 2 | 3 | app = ms.create_app() 4 | 5 | if __name__ == "__main__": 6 | app.run() 7 | -------------------------------------------------------------------------------- /examples/microservice_metrics/swagger.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | swagger: "2.0" 3 | info: 4 | description: "This is a sample server Test server" 5 | version: "1.0.0" 6 | title: "Swagger Test list" 7 | termsOfService: "http://swagger.io/terms/" 8 | contact: 9 | email: "apiteam@swagger.io" 10 | license: 11 | name: "Apache 2.0" 12 | url: "http://www.apache.org/licenses/LICENSE-2.0.html" 13 | tags: 14 | - name: "colors" 15 | description: "Everything about your colors" 16 | externalDocs: 17 | description: "Find out more" 18 | url: "http://swagger.io" 19 | - name: "store" 20 | description: "Example endpoint list of colors" 21 | - name: "user" 22 | description: "Operations about user" 23 | externalDocs: 24 | description: "Find out more about our store" 25 | url: "http://swagger.io" 26 | schemes: 27 | - "http" 28 | paths: 29 | /: 30 | get: 31 | tags: 32 | - "test" 33 | summary: "Example endpoint" 34 | description: "" 35 | operationId: "examples.microservice_metrics.views.example" 36 | consumes: 37 | - "application/json" 38 | produces: 39 | - "application/json" 40 | responses: 41 | "200": 42 | description: "A list of colors (may be filtered by palette)" 43 | schema: 44 | $ref: '#/definitions/Example' 45 | "405": 46 | description: "Invalid input" 47 | definitions: 48 | Example: 49 | type: "object" 50 | properties: 51 | main: 52 | type: "string" 53 | externalDocs: 54 | description: "Find out more about Swagger" 55 | url: "http://swagger.io" -------------------------------------------------------------------------------- /examples/microservice_metrics/views.py: -------------------------------------------------------------------------------- 1 | from flask import current_app 2 | 3 | from examples.microservice_metrics import ms 4 | 5 | 6 | def example(): 7 | current_app.logger.info("start request") 8 | result = ms.requests.get_for_object("https://ghibliapi.herokuapp.com/films/2baf70d1-42bb-4437-b551-e5fed5a87abe") 9 | current_app.logger.info("end request") 10 | return result 11 | -------------------------------------------------------------------------------- /examples/microservice_requests/__init__.py: -------------------------------------------------------------------------------- 1 | from pyms.flask.app import Microservice 2 | 3 | ms = Microservice() 4 | -------------------------------------------------------------------------------- /examples/microservice_requests/config.yml: -------------------------------------------------------------------------------- 1 | pyms: 2 | services: 3 | requests: 4 | data: "" 5 | swagger: 6 | path: "" 7 | file: "swagger.yaml" 8 | config: 9 | DEBUG: true 10 | TESTING: false 11 | APP_NAME: "Python Microservice" 12 | APPLICATION_ROOT: "" -------------------------------------------------------------------------------- /examples/microservice_requests/main.py: -------------------------------------------------------------------------------- 1 | from examples.microservice_requests import ms 2 | 3 | app = ms.create_app() 4 | 5 | if __name__ == "__main__": 6 | app.run() 7 | -------------------------------------------------------------------------------- /examples/microservice_requests/swagger.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | swagger: "2.0" 3 | info: 4 | description: "This is a sample server Test server" 5 | version: "1.0.0" 6 | title: "Swagger Test list" 7 | termsOfService: "http://swagger.io/terms/" 8 | contact: 9 | email: "apiteam@swagger.io" 10 | license: 11 | name: "Apache 2.0" 12 | url: "http://www.apache.org/licenses/LICENSE-2.0.html" 13 | tags: 14 | - name: "colors" 15 | description: "Everything about your colors" 16 | externalDocs: 17 | description: "Find out more" 18 | url: "http://swagger.io" 19 | - name: "store" 20 | description: "Example endpoint list of colors" 21 | - name: "user" 22 | description: "Operations about user" 23 | externalDocs: 24 | description: "Find out more about our store" 25 | url: "http://swagger.io" 26 | schemes: 27 | - "http" 28 | paths: 29 | /: 30 | get: 31 | tags: 32 | - "test" 33 | summary: "Example endpoint" 34 | description: "" 35 | operationId: "examples.microservice_requests.views.example" 36 | consumes: 37 | - "application/json" 38 | produces: 39 | - "application/json" 40 | responses: 41 | "200": 42 | description: "A list of colors (may be filtered by palette)" 43 | schema: 44 | $ref: '#/definitions/Example' 45 | "405": 46 | description: "Invalid input" 47 | definitions: 48 | Example: 49 | type: "object" 50 | properties: 51 | main: 52 | type: "string" 53 | externalDocs: 54 | description: "Find out more about Swagger" 55 | url: "http://swagger.io" -------------------------------------------------------------------------------- /examples/microservice_requests/views.py: -------------------------------------------------------------------------------- 1 | from flask import current_app 2 | 3 | from examples.microservice_requests import ms 4 | 5 | 6 | def example(): 7 | current_app.logger.info("start request") 8 | result = ms.requests.get_for_object("https://ghibliapi.herokuapp.com/films/2baf70d1-42bb-4437-b551-e5fed5a87abe") 9 | current_app.logger.info("end request") 10 | return result 11 | -------------------------------------------------------------------------------- /examples/microservice_service_discovery/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-microservices/pyms/e7c50a127ffe51f67808600a55fddcde7d24664e/examples/microservice_service_discovery/__init__.py -------------------------------------------------------------------------------- /examples/microservice_service_discovery/config.yml: -------------------------------------------------------------------------------- 1 | pyms: 2 | services: 3 | service_discovery: 4 | service: "examples.microservice_service_discovery.service.ServiceDiscoveryConsulBasic" 5 | host: "localhost" 6 | autoregister: true 7 | config: 8 | DEBUG: true 9 | TESTING: false 10 | APP_NAME: "Python Microservice My personal Service Discovery" 11 | APPLICATION_ROOT: "" -------------------------------------------------------------------------------- /examples/microservice_service_discovery/main.py: -------------------------------------------------------------------------------- 1 | from flask import jsonify 2 | 3 | from pyms.flask.app import Microservice 4 | 5 | ms = Microservice(path=__file__) 6 | app = ms.create_app() 7 | 8 | 9 | @app.route("/") 10 | def example(): 11 | return jsonify({"main": "hello world"}) 12 | 13 | 14 | if __name__ == "__main__": 15 | app.run() 16 | -------------------------------------------------------------------------------- /examples/microservice_service_discovery/service.py: -------------------------------------------------------------------------------- 1 | import json 2 | import uuid 3 | 4 | import requests 5 | 6 | from pyms.flask.services.service_discovery import ServiceDiscoveryBase 7 | 8 | 9 | class ServiceDiscoveryConsulBasic(ServiceDiscoveryBase): 10 | id_app = str(uuid.uuid1()) 11 | 12 | def __init__(self, config): 13 | super().__init__(config) 14 | self.host = config.host 15 | self.port = config.port 16 | 17 | def register_service(self, *args, **kwargs): 18 | app_name = kwargs["app_name"] 19 | healtcheck_url = kwargs["healtcheck_url"] 20 | interval = kwargs["interval"] 21 | headers = {"Content-Type": "application/json; charset=utf-8"} 22 | data = { 23 | "id": app_name + "-" + self.id_app, 24 | "name": app_name, 25 | "check": {"name": "ping check", "http": healtcheck_url, "interval": interval, "status": "passing"}, 26 | } 27 | response = requests.put( 28 | "http://{host}:{port}/v1/agent/service/register".format(host=self.host, port=self.port), 29 | data=json.dumps(data), 30 | headers=headers, 31 | ) 32 | if response.status_code != 200: 33 | raise Exception(response.content) 34 | -------------------------------------------------------------------------------- /examples/microservice_service_discovery_consul/config.yml: -------------------------------------------------------------------------------- 1 | pyms: 2 | services: 3 | service_discovery: 4 | service: "consul" 5 | host: "localhost" 6 | autoregister: true 7 | config: 8 | DEBUG: true 9 | TESTING: false 10 | APP_NAME: "Python Microservice" 11 | APPLICATION_ROOT: "" -------------------------------------------------------------------------------- /examples/microservice_service_discovery_consul/main.py: -------------------------------------------------------------------------------- 1 | from flask import jsonify 2 | 3 | from pyms.flask.app import Microservice 4 | 5 | ms = Microservice(path=__file__) 6 | app = ms.create_app() 7 | 8 | 9 | @app.route("/") 10 | def example(): 11 | checks = ms.service_discovery.client.agent.checks() 12 | return jsonify({"main": checks}) 13 | 14 | 15 | if __name__ == "__main__": 16 | app.run() 17 | -------------------------------------------------------------------------------- /examples/microservice_swagger/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-microservices/pyms/e7c50a127ffe51f67808600a55fddcde7d24664e/examples/microservice_swagger/__init__.py -------------------------------------------------------------------------------- /examples/microservice_swagger/config.yml: -------------------------------------------------------------------------------- 1 | pyms: 2 | services: 3 | swagger: 4 | path: "" 5 | file: "swagger.yaml" 6 | url: "ws-doc/" 7 | config: 8 | DEBUG: true 9 | TESTING: false 10 | APP_NAME: "Python Microservice" 11 | APPLICATION_ROOT: "" -------------------------------------------------------------------------------- /examples/microservice_swagger/main.py: -------------------------------------------------------------------------------- 1 | from pyms.flask.app import Microservice 2 | 3 | ms = Microservice() 4 | app = ms.create_app() 5 | 6 | if __name__ == "__main__": 7 | app.run() 8 | -------------------------------------------------------------------------------- /examples/microservice_swagger/swagger.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | swagger: "2.0" 3 | info: 4 | description: "This is a sample server Test server" 5 | version: "1.0.0" 6 | title: "Swagger Test list" 7 | termsOfService: "http://swagger.io/terms/" 8 | contact: 9 | email: "apiteam@swagger.io" 10 | license: 11 | name: "Apache 2.0" 12 | url: "http://www.apache.org/licenses/LICENSE-2.0.html" 13 | tags: 14 | - name: "colors" 15 | description: "Everything about your colors" 16 | externalDocs: 17 | description: "Find out more" 18 | url: "http://swagger.io" 19 | - name: "store" 20 | description: "Example endpoint list of colors" 21 | - name: "user" 22 | description: "Operations about user" 23 | externalDocs: 24 | description: "Find out more about our store" 25 | url: "http://swagger.io" 26 | schemes: 27 | - "http" 28 | paths: 29 | /: 30 | get: 31 | tags: 32 | - "test" 33 | summary: "Example endpoint" 34 | description: "" 35 | operationId: "examples.microservice_swagger.views.example" 36 | consumes: 37 | - "application/json" 38 | produces: 39 | - "application/json" 40 | responses: 41 | "200": 42 | description: "A list of colors (may be filtered by palette)" 43 | schema: 44 | $ref: '#/definitions/Example' 45 | "405": 46 | description: "Invalid input" 47 | definitions: 48 | Example: 49 | type: "object" 50 | properties: 51 | main: 52 | type: "string" 53 | externalDocs: 54 | description: "Find out more about Swagger" 55 | url: "http://swagger.io" -------------------------------------------------------------------------------- /examples/microservice_swagger/views.py: -------------------------------------------------------------------------------- 1 | def example(): 2 | return {"main": "hello world"} 3 | -------------------------------------------------------------------------------- /examples/microservice_tracer/__init__.py: -------------------------------------------------------------------------------- 1 | from pyms.flask.app import Microservice 2 | 3 | ms = Microservice(path=__file__) 4 | -------------------------------------------------------------------------------- /examples/microservice_tracer/config.yml: -------------------------------------------------------------------------------- 1 | pyms: 2 | services: 3 | metrics: true 4 | requests: 5 | data: "" 6 | swagger: 7 | path: "" 8 | file: "swagger.yaml" 9 | tracer: 10 | client: "jaeger" 11 | host: "localhost" 12 | component_name: "Python Microservice" 13 | config: 14 | DEBUG: true 15 | TESTING: false 16 | APP_NAME: "Python Microservice" 17 | APPLICATION_ROOT: "" -------------------------------------------------------------------------------- /examples/microservice_tracer/main.py: -------------------------------------------------------------------------------- 1 | from examples.microservice_requests import ms 2 | 3 | app = ms.create_app() 4 | 5 | if __name__ == "__main__": 6 | app.run() 7 | -------------------------------------------------------------------------------- /examples/microservice_tracer/swagger.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | swagger: "2.0" 3 | info: 4 | description: "This is a sample server Test server" 5 | version: "1.0.0" 6 | title: "Swagger Test list" 7 | termsOfService: "http://swagger.io/terms/" 8 | contact: 9 | email: "apiteam@swagger.io" 10 | license: 11 | name: "Apache 2.0" 12 | url: "http://www.apache.org/licenses/LICENSE-2.0.html" 13 | tags: 14 | - name: "colors" 15 | description: "Everything about your colors" 16 | externalDocs: 17 | description: "Find out more" 18 | url: "http://swagger.io" 19 | - name: "store" 20 | description: "Example endpoint list of colors" 21 | - name: "user" 22 | description: "Operations about user" 23 | externalDocs: 24 | description: "Find out more about our store" 25 | url: "http://swagger.io" 26 | schemes: 27 | - "http" 28 | paths: 29 | /: 30 | get: 31 | tags: 32 | - "test" 33 | summary: "Example endpoint" 34 | description: "" 35 | operationId: "examples.microservice_requests.views.example" 36 | consumes: 37 | - "application/json" 38 | produces: 39 | - "application/json" 40 | responses: 41 | "200": 42 | description: "A list of colors (may be filtered by palette)" 43 | schema: 44 | $ref: '#/definitions/Example' 45 | "405": 46 | description: "Invalid input" 47 | definitions: 48 | Example: 49 | type: "object" 50 | properties: 51 | main: 52 | type: "string" 53 | externalDocs: 54 | description: "Find out more about Swagger" 55 | url: "http://swagger.io" -------------------------------------------------------------------------------- /examples/microservice_tracer/views.py: -------------------------------------------------------------------------------- 1 | from flask import current_app 2 | 3 | from examples.microservice_requests import ms 4 | 5 | 6 | def example(): 7 | current_app.logger.info("start request") 8 | result = ms.requests.get_for_object("https://ghibliapi.herokuapp.com/films/2baf70d1-42bb-4437-b551-e5fed5a87abe") 9 | current_app.logger.info("end request") 10 | return result 11 | -------------------------------------------------------------------------------- /examples/mininum_microservice/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-microservices/pyms/e7c50a127ffe51f67808600a55fddcde7d24664e/examples/mininum_microservice/__init__.py -------------------------------------------------------------------------------- /examples/mininum_microservice/config.yml: -------------------------------------------------------------------------------- 1 | pyms: 2 | config: 3 | DEBUG: true 4 | TESTING: false 5 | SWAGGER: true 6 | APP_NAME: business-glossary 7 | APPLICATION_ROOT : "" 8 | SECRET_KEY: "gjr39dkjn344_!67#" -------------------------------------------------------------------------------- /examples/mininum_microservice/main.py: -------------------------------------------------------------------------------- 1 | from flask import jsonify 2 | 3 | from pyms.flask.app import Microservice 4 | 5 | ms = Microservice(path=__file__) 6 | app = ms.create_app() 7 | 8 | 9 | @app.route("/") 10 | def example(): 11 | return jsonify({"main": "hello world"}) 12 | 13 | 14 | if __name__ == "__main__": 15 | app.run() 16 | -------------------------------------------------------------------------------- /examples/mininum_microservice_docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.6.4-alpine3.7 2 | 3 | RUN apk add --update curl gcc g++ git libffi-dev openssl-dev python3-dev build-base linux-headers \ 4 | && rm -rf /var/cache/apk/* 5 | 6 | ENV PYTHONUNBUFFERED=1 APP_HOME=/microservice/ 7 | ENV PYMS_CONFIGMAP_FILE="$APP_HOME"config-docker.yml 8 | 9 | RUN mkdir $APP_HOME && adduser -S -D -H python 10 | RUN chown -R python $APP_HOME 11 | WORKDIR $APP_HOME 12 | 13 | RUN pip install --upgrade pip 14 | RUN pip install -r py-ms gunicorn gevent 15 | 16 | 17 | ADD . $APP_HOME 18 | 19 | EXPOSE 5000 20 | USER python 21 | 22 | CMD ["gunicorn", "--worker-class", "gevent", "--workers", "8", "--log-level", "INFO", "--bind", "0.0.0.0:5000", "manage:app"] -------------------------------------------------------------------------------- /examples/mininum_microservice_docker/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-microservices/pyms/e7c50a127ffe51f67808600a55fddcde7d24664e/examples/mininum_microservice_docker/__init__.py -------------------------------------------------------------------------------- /examples/mininum_microservice_docker/config-docker.yml: -------------------------------------------------------------------------------- 1 | pyms: 2 | config: 3 | app_name: "Python Microservice" 4 | environment: "I'm running in docker" -------------------------------------------------------------------------------- /examples/mininum_microservice_docker/config.yml: -------------------------------------------------------------------------------- 1 | pyms: 2 | config: 3 | app_name: "Python Microservice" 4 | environment: "I'm running in a local machine" -------------------------------------------------------------------------------- /examples/mininum_microservice_docker/main.py: -------------------------------------------------------------------------------- 1 | from flask import jsonify 2 | 3 | from pyms.flask.app import Microservice 4 | 5 | ms = Microservice(service="my-minimal-microservice", path=__file__) 6 | app = ms.create_app() 7 | 8 | 9 | @app.route("/") 10 | def example(): 11 | return jsonify({"main": ms.config.environment}) 12 | 13 | 14 | if __name__ == "__main__": 15 | app.run() 16 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: PyMS 2 | site_url: https://github.com/python-microservices/pyms 3 | repo_url: https://github.com/python-microservices/pyms 4 | repo_name: python-microservices/pyms 5 | site_author: Alberto Vara -------------------------------------------------------------------------------- /pylintrc: -------------------------------------------------------------------------------- 1 | [MASTER] 2 | 3 | # A comma-separated list of package or module names from where C extensions may 4 | # be loaded. Extensions are loading into the active Python interpreter and may 5 | # run arbitrary code 6 | extension-pkg-whitelist= 7 | 8 | # Add files or directories to the blacklist. They should be base names, not 9 | # paths. 10 | ignore=CVS 11 | 12 | # Add files or directories matching the regex patterns to the blacklist. The 13 | # regex matches against base names, not paths. 14 | ignore-patterns= 15 | 16 | # Python code to execute, usually for sys.path manipulation such as 17 | # pygtk.require(). 18 | #init-hook= 19 | 20 | # Use multiple processes to speed up Pylint. 21 | jobs=1 22 | 23 | # List of plugins (as comma separated values of python modules names) to load, 24 | # usually to register additional checkers. 25 | load-plugins= 26 | 27 | # Pickle collected data for later comparisons. 28 | persistent=yes 29 | 30 | # Specify a configuration file. 31 | #rcfile= 32 | 33 | # When enabled, pylint would attempt to guess common misconfiguration and emit 34 | # user-friendly hints instead of false-positive error messages 35 | suggestion-mode=yes 36 | 37 | # Allow loading of arbitrary C extensions. Extensions are imported into the 38 | # active Python interpreter and may run arbitrary code. 39 | unsafe-load-any-extension=no 40 | 41 | 42 | [MESSAGES CONTROL] 43 | 44 | # Only show warnings with the listed confidence levels. Leave empty to show 45 | # all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED 46 | confidence= 47 | 48 | # Disable the message, report, category or checker with the given id(s). You 49 | # can either give multiple identifiers separated by comma (,) or put this 50 | # option multiple times (only on the command line, not in the configuration 51 | # file where it should appear only once).You can also use "--disable=all" to 52 | # disable everything first and then reenable specific checks. For example, if 53 | # you want to run only the similarities checker, you can use "--disable=all 54 | # --enable=similarities". If you want to run only the classes checker, but have 55 | # no Warning level messages displayed, use"--disable=all --enable=classes 56 | # --disable=W" 57 | disable=logging-format-interpolation,broad-except,unnecessary-pass,no-member,line-too-long,invalid-name, 58 | missing-module-docstring,missing-class-docstring,missing-function-docstring,too-few-public-methods, 59 | consider-using-f-string,deprecated-class,unnecessary-dunder-call,deprecated-module 60 | 61 | # Enable the message, report, category or checker with the given id(s). You can 62 | # either give multiple identifier separated by comma (,) or put this option 63 | # multiple time (only on the command line, not in the configuration file where 64 | # it should appear only once). See also the "--disable" option for examples. 65 | enable=c-extension-no-member 66 | 67 | 68 | [REPORTS] 69 | 70 | # Python expression which should return a note less than 10 (10 is the highest 71 | # note). You have access to the variables errors warning, statement which 72 | # respectively contain the number of errors / warnings messages and the total 73 | # number of statements analyzed. This is used by the global evaluation report 74 | # (RP0004). 75 | evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) 76 | 77 | # Template used to display messages. This is a python new-style format string 78 | # used to format the message information. See doc for all details 79 | #msg-template= 80 | 81 | # Set the output format. Available formats are text, parseable, colorized, json 82 | # and msvs (visual studio).You can also give a reporter class, eg 83 | # mypackage.mymodule.MyReporterClass. 84 | output-format=text 85 | 86 | # Tells whether to display a full report or only the messages 87 | reports=no 88 | 89 | # Activate the evaluation score. 90 | score=yes 91 | 92 | 93 | [REFACTORING] 94 | 95 | # Maximum number of nested blocks for function / method body 96 | max-nested-blocks=5 97 | 98 | # Complete name of functions that never returns. When checking for 99 | # inconsistent-return-statements if a never returning function is called then 100 | # it will be considered as an explicit return statement and no message will be 101 | # printed. 102 | never-returning-functions=optparse.Values,sys.exit 103 | 104 | 105 | [BASIC] 106 | 107 | # Naming style matching correct argument names 108 | argument-naming-style=snake_case 109 | 110 | # Regular expression matching correct argument names. Overrides argument- 111 | # naming-style 112 | #argument-rgx= 113 | 114 | # Naming style matching correct attribute names 115 | attr-naming-style=snake_case 116 | 117 | # Regular expression matching correct attribute names. Overrides attr-naming- 118 | # style 119 | #attr-rgx= 120 | 121 | # Bad variable names which should always be refused, separated by a comma 122 | bad-names=foo, 123 | bar, 124 | baz, 125 | toto, 126 | tutu, 127 | tata 128 | 129 | # Naming style matching correct class attribute names 130 | class-attribute-naming-style=any 131 | 132 | # Regular expression matching correct class attribute names. Overrides class- 133 | # attribute-naming-style 134 | #class-attribute-rgx= 135 | 136 | # Naming style matching correct class names 137 | class-naming-style=PascalCase 138 | 139 | # Regular expression matching correct class names. Overrides class-naming-style 140 | #class-rgx= 141 | 142 | # Naming style matching correct constant names 143 | const-naming-style=UPPER_CASE 144 | 145 | # Regular expression matching correct constant names. Overrides const-naming- 146 | # style 147 | #const-rgx= 148 | 149 | # Minimum line length for functions/classes that require docstrings, shorter 150 | # ones are exempt. 151 | docstring-min-length=-1 152 | 153 | # Naming style matching correct function names 154 | function-naming-style=snake_case 155 | 156 | # Regular expression matching correct function names. Overrides function- 157 | # naming-style 158 | #function-rgx= 159 | 160 | # Good variable names which should always be accepted, separated by a comma 161 | good-names=i, 162 | j, 163 | k, 164 | ex, 165 | Run, 166 | _ 167 | 168 | # Include a hint for the correct naming format with invalid-name 169 | include-naming-hint=no 170 | 171 | # Naming style matching correct inline iteration names 172 | inlinevar-naming-style=any 173 | 174 | # Regular expression matching correct inline iteration names. Overrides 175 | # inlinevar-naming-style 176 | #inlinevar-rgx= 177 | 178 | # Naming style matching correct method names 179 | method-naming-style=snake_case 180 | 181 | # Regular expression matching correct method names. Overrides method-naming- 182 | # style 183 | #method-rgx= 184 | 185 | # Naming style matching correct module names 186 | module-naming-style=snake_case 187 | 188 | # Regular expression matching correct module names. Overrides module-naming- 189 | # style 190 | #module-rgx= 191 | 192 | # Colon-delimited sets of names that determine each other's naming style when 193 | # the name regexes allow several styles. 194 | name-group= 195 | 196 | # Regular expression which should only match function or class names that do 197 | # not require a docstring. 198 | no-docstring-rgx=^_ 199 | 200 | # List of decorators that produce properties, such as abc.abstractproperty. Add 201 | # to this list to register other decorators that produce valid properties. 202 | property-classes=abc.abstractproperty 203 | 204 | # Naming style matching correct variable names 205 | variable-naming-style=snake_case 206 | 207 | # Regular expression matching correct variable names. Overrides variable- 208 | # naming-style 209 | #variable-rgx= 210 | 211 | 212 | [LOGGING] 213 | 214 | # Logging modules to check that the string format arguments are in logging 215 | # function parameter format 216 | logging-modules=logging 217 | 218 | 219 | [SPELLING] 220 | 221 | # Limits count of emitted suggestions for spelling mistakes 222 | max-spelling-suggestions=4 223 | 224 | # Spelling dictionary name. Available dictionaries: none. To make it working 225 | # install python-enchant package. 226 | spelling-dict= 227 | 228 | # List of comma separated words that should not be checked. 229 | spelling-ignore-words= 230 | 231 | # A path to a file that contains private dictionary; one word per line. 232 | spelling-private-dict-file= 233 | 234 | # Tells whether to store unknown words to indicated private dictionary in 235 | # --spelling-private-dict-file option instead of raising a message. 236 | spelling-store-unknown-words=no 237 | 238 | 239 | [FORMAT] 240 | 241 | # Expected format of line ending, e.g. empty (any line ending), LF or CRLF. 242 | expected-line-ending-format= 243 | 244 | # Regexp for a line that is allowed to be longer than the limit. 245 | ignore-long-lines=^\s*(# )??$ 246 | 247 | # Number of spaces of indent required inside a hanging or continued line. 248 | indent-after-paren=4 249 | 250 | # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 251 | # tab). 252 | indent-string=' ' 253 | 254 | # Maximum number of characters on a single line. 255 | max-line-length=120 256 | 257 | # Maximum number of lines in a module 258 | max-module-lines=1000 259 | 260 | 261 | # Allow the body of a class to be on the same line as the declaration if body 262 | # contains single statement. 263 | single-line-class-stmt=no 264 | 265 | # Allow the body of an if to be on the same line as the test if there is no 266 | # else. 267 | single-line-if-stmt=no 268 | 269 | 270 | [SIMILARITIES] 271 | 272 | # Ignore comments when computing similarities. 273 | ignore-comments=yes 274 | 275 | # Ignore docstrings when computing similarities. 276 | ignore-docstrings=yes 277 | 278 | # Ignore imports when computing similarities. 279 | ignore-imports=no 280 | 281 | # Minimum lines number of a similarity. 282 | min-similarity-lines=4 283 | 284 | 285 | [MISCELLANEOUS] 286 | 287 | # List of note tags to take in consideration, separated by a comma. 288 | notes=FIXME, 289 | XXX 290 | 291 | 292 | [TYPECHECK] 293 | 294 | # List of decorators that produce context managers, such as 295 | # contextlib.contextmanager. Add to this list to register other decorators that 296 | # produce valid context managers. 297 | contextmanager-decorators=contextlib.contextmanager 298 | 299 | # List of members which are set dynamically and missed by pylint inference 300 | # system, and so shouldn't trigger E1101 when accessed. Python regular 301 | # expressions are accepted. 302 | generated-members= 303 | 304 | # Tells whether missing members accessed in mixin class should be ignored. A 305 | # mixin class is detected if its name ends with "mixin" (case insensitive). 306 | ignore-mixin-members=yes 307 | 308 | # This flag controls whether pylint should warn about no-member and similar 309 | # checks whenever an opaque object is returned when inferring. The inference 310 | # can return multiple potential results while evaluating a Python object, but 311 | # some branches might not be evaluated, which results in partial inference. In 312 | # that case, it might be useful to still emit no-member and other checks for 313 | # the rest of the inferred objects. 314 | ignore-on-opaque-inference=yes 315 | 316 | # List of class names for which member attributes should not be checked (useful 317 | # for classes with dynamically set attributes). This supports the use of 318 | # qualified names. 319 | ignored-classes=optparse.Values,thread._local,_thread._local 320 | 321 | # List of module names for which member attributes should not be checked 322 | # (useful for modules/projects where namespaces are manipulated during runtime 323 | # and thus existing member attributes cannot be deduced by static analysis. It 324 | # supports qualified module names, as well as Unix pattern matching. 325 | ignored-modules= 326 | 327 | # Show a hint with possible names when a member name was not found. The aspect 328 | # of finding the hint is based on edit distance. 329 | missing-member-hint=yes 330 | 331 | # The minimum edit distance a name should have in order to be considered a 332 | # similar match for a missing member name. 333 | missing-member-hint-distance=1 334 | 335 | # The total number of similar names that should be taken in consideration when 336 | # showing a hint for a missing member. 337 | missing-member-max-choices=1 338 | 339 | 340 | [VARIABLES] 341 | 342 | # List of additional names supposed to be defined in builtins. Remember that 343 | # you should avoid to define new builtins when possible. 344 | additional-builtins= 345 | 346 | # Tells whether unused global variables should be treated as a violation. 347 | allow-global-unused-variables=yes 348 | 349 | # List of strings which can identify a callback function by name. A callback 350 | # name must start or end with one of those strings. 351 | callbacks=cb_, 352 | _cb 353 | 354 | # A regular expression matching the name of dummy variables (i.e. expectedly 355 | # not used). 356 | dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ 357 | 358 | # Argument names that match this expression will be ignored. Default to name 359 | # with leading underscore 360 | ignored-argument-names=arg|args|kwargs|_.*|^ignored_|^unused_ 361 | 362 | # Tells whether we should check for unused import in __init__ files. 363 | init-import=no 364 | 365 | # List of qualified module names which can have objects that can redefine 366 | # builtins. 367 | redefining-builtins-modules=six.moves,past.builtins,future.builtins 368 | 369 | 370 | [CLASSES] 371 | 372 | # List of method names used to declare (i.e. assign) instance attributes. 373 | defining-attr-methods=__init__, 374 | __new__, 375 | setUp 376 | 377 | # List of member names, which should be excluded from the protected access 378 | # warning. 379 | exclude-protected=_asdict, 380 | _fields, 381 | _replace, 382 | _source, 383 | _make 384 | 385 | # List of valid names for the first argument in a class method. 386 | valid-classmethod-first-arg=cls 387 | 388 | # List of valid names for the first argument in a metaclass class method. 389 | valid-metaclass-classmethod-first-arg=mcs 390 | 391 | 392 | [DESIGN] 393 | 394 | # Maximum number of arguments for function / method 395 | max-args=7 396 | 397 | # Maximum number of attributes for a class (see R0902). 398 | max-attributes=8 399 | 400 | # Maximum number of boolean expressions in a if statement 401 | max-bool-expr=5 402 | 403 | # Maximum number of branch for function / method body 404 | max-branches=12 405 | 406 | # Maximum number of locals for function / method body 407 | max-locals=15 408 | 409 | # Maximum number of parents for a class (see R0901). 410 | max-parents=7 411 | 412 | # Maximum number of public methods for a class (see R0904). 413 | max-public-methods=20 414 | 415 | # Maximum number of return / yield for function / method body 416 | max-returns=6 417 | 418 | # Maximum number of statements in function / method body 419 | max-statements=50 420 | 421 | # Minimum number of public methods for a class (see R0903). 422 | min-public-methods=2 423 | 424 | 425 | [IMPORTS] 426 | 427 | # Allow wildcard imports from modules that define __all__. 428 | allow-wildcard-with-all=no 429 | 430 | # Analyse import fallback blocks. This can be used to support both Python 2 and 431 | # 3 compatible code, which means that the block might have code that exists 432 | # only in one or another interpreter, leading to false positives when analysed. 433 | analyse-fallback-blocks=no 434 | 435 | # Deprecated modules which should not be used, separated by a comma 436 | deprecated-modules=optparse,tkinter.tix 437 | 438 | # Create a graph of external dependencies in the given file (report RP0402 must 439 | # not be disabled) 440 | ext-import-graph= 441 | 442 | # Create a graph of every (i.e. internal and external) dependencies in the 443 | # given file (report RP0402 must not be disabled) 444 | import-graph= 445 | 446 | # Create a graph of internal dependencies in the given file (report RP0402 must 447 | # not be disabled) 448 | int-import-graph= 449 | 450 | # Force import order to recognize a module as part of the standard 451 | # compatibility libraries. 452 | known-standard-library= 453 | 454 | # Force import order to recognize a module as part of a third party library. 455 | known-third-party=enchant 456 | 457 | 458 | [EXCEPTIONS] 459 | 460 | # Exceptions that will emit a warning when being caught. Defaults to 461 | # "Exception" 462 | overgeneral-exceptions=Exception 463 | -------------------------------------------------------------------------------- /pyms/__init__.py: -------------------------------------------------------------------------------- 1 | __author__ = "Alberto Vara" 2 | 3 | __email__ = "a.vara.1986@gmail.com" 4 | 5 | __version__ = "2.9.0rc1" 6 | -------------------------------------------------------------------------------- /pyms/cloud/aws/kms.py: -------------------------------------------------------------------------------- 1 | import base64 2 | 3 | from pyms.crypt.driver import CryptAbstract 4 | from pyms.utils import check_package_exists, import_package 5 | 6 | 7 | class Crypt(CryptAbstract): 8 | encryption_algorithm = "SYMMETRIC_DEFAULT" # 'SYMMETRIC_DEFAULT' | 'RSAES_OAEP_SHA_1' | 'RSAES_OAEP_SHA_256' 9 | key_id = "" 10 | 11 | def __init__(self, *args, **kwargs) -> None: 12 | self._init_boto() 13 | super().__init__(*args, **kwargs) 14 | 15 | def encrypt(self, message: str) -> str: # pragma: no cover 16 | ciphertext = self.client.encrypt( 17 | KeyId=self.config.key_id, 18 | Plaintext=bytes(message, encoding="UTF-8"), 19 | ) 20 | return str(base64.b64encode(ciphertext["CiphertextBlob"]), encoding="UTF-8") 21 | 22 | def _init_boto(self) -> None: # pragma: no cover 23 | check_package_exists("boto3") 24 | boto3 = import_package("boto3") 25 | boto3.set_stream_logger(name="botocore") 26 | self.client = boto3.client("kms") 27 | 28 | def _aws_decrypt(self, blob_text: bytes) -> str: # pragma: no cover 29 | response = self.client.decrypt( 30 | CiphertextBlob=blob_text, KeyId=self.config.key_id, EncryptionAlgorithm=self.encryption_algorithm 31 | ) 32 | return str(response["Plaintext"], encoding="UTF-8") 33 | 34 | def _parse_encrypted(self, encrypted: str) -> bytes: 35 | blob_text = base64.b64decode(encrypted) 36 | return blob_text 37 | 38 | def decrypt(self, encrypted: str) -> str: 39 | blob_text = self._parse_encrypted(encrypted) 40 | decrypted = self._aws_decrypt(blob_text) 41 | 42 | return decrypted 43 | -------------------------------------------------------------------------------- /pyms/cmd/__init__.py: -------------------------------------------------------------------------------- 1 | from .main import Command 2 | 3 | __all__ = ["Command"] 4 | -------------------------------------------------------------------------------- /pyms/cmd/main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from __future__ import print_function, unicode_literals 3 | 4 | import argparse 5 | import os 6 | import sys 7 | 8 | from pyms.config import create_conf_file 9 | from pyms.crypt.fernet import Crypt 10 | from pyms.flask.services.swagger import merge_swagger_file 11 | from pyms.utils import check_package_exists, import_from, utils 12 | 13 | 14 | def _asbool(value): 15 | """Convert the given String to a boolean object. 16 | 17 | Accepted values are `True` and `1`. 18 | """ 19 | if value is None: 20 | return False 21 | 22 | if isinstance(value, bool): 23 | return value 24 | 25 | return value.lower() in ("true", "1") 26 | 27 | class Command: 28 | config = None 29 | 30 | parser = None 31 | 32 | args = [] 33 | 34 | # flake8: noqa: C901 35 | def __init__(self, *args, **kwargs): 36 | arguments = kwargs.get("arguments", False) 37 | autorun = kwargs.get("autorun", True) 38 | if not arguments: # pragma: no cover 39 | arguments = sys.argv[1:] 40 | 41 | parser = argparse.ArgumentParser(description="Python Microservices") 42 | 43 | commands = parser.add_subparsers(title="Commands", description="Available commands", dest="command_name") 44 | 45 | parser_encrypt = commands.add_parser("encrypt", help="Encrypt a string") 46 | parser_encrypt.add_argument("encrypt", default="", type=str, help="Encrypt a string") 47 | 48 | parser_create_key = commands.add_parser("create-key", help="Generate a Key to encrypt strings in config") 49 | parser_create_key.add_argument( 50 | "create_key", action="store_true", help="Generate a Key to encrypt strings in config" 51 | ) 52 | 53 | parser_startproject = commands.add_parser( 54 | "startproject", 55 | help="Generate a project from https://github.com/python-microservices/microservices-template", 56 | ) 57 | parser_startproject.add_argument( 58 | "startproject", 59 | action="store_true", 60 | help="Generate a project from https://github.com/python-microservices/microservices-template", 61 | ) 62 | 63 | parser_startproject.add_argument( 64 | "-b", "--branch", help="Select a branch from https://github.com/python-microservices/microservices-template" 65 | ) 66 | 67 | parser_merge_swagger = commands.add_parser("merge-swagger", help="Merge swagger into a single file") 68 | parser_merge_swagger.add_argument("merge_swagger", action="store_true", help="Merge swagger into a single file") 69 | parser_merge_swagger.add_argument( 70 | "-f", "--file", default=os.path.join("project", "swagger", "swagger.yaml"), help="Swagger file path" 71 | ) 72 | 73 | parser_create_config = commands.add_parser("create-config", help="Generate a config file") 74 | parser_create_config.add_argument("create_config", action="store_true", help="Generate a config file") 75 | 76 | parser.add_argument("-v", "--verbose", default="", type=str, help="Verbose ") 77 | 78 | args = parser.parse_args(arguments) 79 | try: 80 | self.create_key = args.create_key 81 | except AttributeError: 82 | self.create_key = False 83 | try: 84 | self.encrypt = args.encrypt 85 | except AttributeError: 86 | self.encrypt = "" 87 | try: 88 | self.startproject = args.startproject 89 | self.branch = args.branch 90 | except AttributeError: 91 | self.startproject = False 92 | try: 93 | self.merge_swagger = args.merge_swagger 94 | self.file = args.file 95 | except AttributeError: 96 | self.merge_swagger = False 97 | try: 98 | self.create_config = args.create_config 99 | except Exception: 100 | self.create_config = False 101 | self.verbose = len(args.verbose) 102 | if autorun: # pragma: no cover 103 | result = self.run() 104 | if result: 105 | self.exit_ok("OK") 106 | else: 107 | self.print_error("ERROR") 108 | 109 | @staticmethod 110 | def get_input(msg): # pragma: no cover 111 | return input(msg) # nosec 112 | 113 | def run(self): 114 | crypt = Crypt() 115 | if self.create_key: 116 | path = crypt._loader.get_path_from_env() # pylint: disable=protected-access 117 | pwd = self.get_input("Type a password to generate the key file: ") 118 | # Should use yes_no_input insted of get input below 119 | # the result should be validated for Yes (Y|y) rather allowing anything other than 'n' 120 | generate_file = self.get_input("Do you want to generate a file in {}? [Y/n]".format(path)) 121 | generate_file = generate_file.lower() != "n" 122 | key = crypt.generate_key(pwd, generate_file) 123 | if generate_file: 124 | self.print_ok("File {} generated OK".format(path)) 125 | else: 126 | self.print_ok("Key generated: {}".format(key)) 127 | if self.encrypt: 128 | # Spoted Unhandle exceptions - The encrypt function throws FileDoesNotExistException, ValueError 129 | # which are not currently handled 130 | encrypted = crypt.encrypt(self.encrypt) 131 | self.print_ok("Encrypted OK: {}".format(encrypted)) 132 | if self.startproject: 133 | check_package_exists("cookiecutter") 134 | cookiecutter = import_from("cookiecutter.main", "cookiecutter") 135 | cookiecutter("gh:python-microservices/cookiecutter-pyms", checkout=self.branch) 136 | self.print_ok("Created project OK") 137 | if self.merge_swagger: 138 | try: 139 | merge_swagger_file(main_file=self.file) 140 | self.print_ok("Swagger file generated [swagger-complete.yaml]") 141 | except FileNotFoundError as ex: 142 | self.print_error(ex.__str__()) 143 | return False 144 | if self.create_config: 145 | use_requests = self.yes_no_input("Do you want to use request") 146 | use_swagger = self.yes_no_input("Do you want to use swagger") 147 | try: 148 | conf_file_path = create_conf_file(use_requests, use_swagger) 149 | self.print_ok(f'Config file "{conf_file_path}" created') 150 | return True 151 | except Exception as ex: 152 | self.print_error(ex.__str__()) 153 | return False 154 | return True 155 | 156 | def yes_no_input(self, msg=""): # pragma: no cover 157 | answer = input( # nosec 158 | utils.colored_text(f'{msg}{"?" if not msg.endswith("?") else ""} [Y/n] :', utils.Colors.BLUE, True) 159 | ) 160 | try: 161 | return _asbool(answer) 162 | except ValueError: 163 | self.print_error('Invalid input, Please answer with a "Y" or "n"') 164 | self.yes_no_input(msg) 165 | return False 166 | 167 | @staticmethod 168 | def print_ok(msg=""): 169 | print(utils.colored_text(msg, utils.Colors.BRIGHT_GREEN, True)) 170 | 171 | def print_verbose(self, msg=""): # pragma: no cover 172 | if self.verbose: 173 | print(msg) 174 | 175 | @staticmethod 176 | def print_error(msg=""): # pragma: no cover 177 | print(utils.colored_text(msg, utils.Colors.BRIGHT_RED, True)) 178 | 179 | def exit_with_error(self, msg=""): # pragma: no cover 180 | self.print_error(msg) 181 | sys.exit(2) 182 | 183 | def exit_ok(self, msg=""): # pragma: no cover 184 | self.print_ok(msg) 185 | sys.exit(0) 186 | 187 | 188 | if __name__ == "__main__": # pragma: no cover 189 | cmd = Command(arguments=sys.argv[1:], autorun=False) 190 | cmd.run() 191 | -------------------------------------------------------------------------------- /pyms/config/__init__.py: -------------------------------------------------------------------------------- 1 | from .conf import create_conf_file, get_conf 2 | from .confile import ConfFile 3 | 4 | __all__ = ["get_conf", "create_conf_file", "ConfFile"] 5 | -------------------------------------------------------------------------------- /pyms/config/conf.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | from typing import Union 4 | 5 | import yaml 6 | 7 | from pyms.config.confile import ConfFile 8 | from pyms.constants import ( 9 | CONFIGMAP_FILE_ENVIRONMENT, 10 | CONFIGMAP_FILE_ENVIRONMENT_LEGACY, 11 | CRYPT_FILE_KEY_ENVIRONMENT, 12 | CRYPT_FILE_KEY_ENVIRONMENT_LEGACY, 13 | DEFAULT_CONFIGMAP_FILENAME, 14 | LOGGER_NAME, 15 | PYMS_CONFIG_WHITELIST_KEYWORDS, 16 | ) 17 | from pyms.exceptions import AttrDoesNotExistException, ConfigErrorException, ServiceDoesNotExistException 18 | from pyms.utils import utils 19 | 20 | logger = logging.getLogger(LOGGER_NAME) 21 | 22 | 23 | def get_conf(*args, **kwargs): 24 | """ 25 | Returns an object with a set of attributes retrieved from the configuration file. Each subblock is a append of the 26 | parent and this name, in example of the next yaml, tracer will be `pyms.tracer`. If we have got his config file: 27 | See these docs: 28 | * https://python-microservices.github.io/configuration/ 29 | * https://python-microservices.github.io/services/ 30 | :param args: 31 | :param kwargs: 32 | 33 | :return: 34 | """ 35 | service = kwargs.pop("service", None) 36 | if not service: 37 | raise ServiceDoesNotExistException("Service not defined") 38 | config = ConfFile(*args, **kwargs) 39 | return getattr(config, service) 40 | 41 | 42 | def validate_conf(*args, **kwargs): 43 | 44 | config = ConfFile(*args, **kwargs) 45 | is_config_ok = True 46 | try: 47 | config.pyms 48 | except AttrDoesNotExistException: 49 | is_config_ok = False 50 | if not is_config_ok: 51 | raise ConfigErrorException( 52 | """Config file must start with `pyms` keyword, for example: 53 | pyms: 54 | services: 55 | metrics: true 56 | requests: 57 | data: data 58 | swagger: 59 | path: "" 60 | file: "swagger.yaml" 61 | tracer: 62 | client: "jaeger" 63 | host: "localhost" 64 | component_name: "Python Microservice" 65 | config: 66 | DEBUG: true 67 | TESTING: true""" 68 | ) 69 | try: 70 | config.pyms.config 71 | except AttrDoesNotExistException: 72 | is_config_ok = False 73 | if not is_config_ok: 74 | raise ConfigErrorException( 75 | """`pyms` block must contain a `config` keyword in your Config file, for example: 76 | pyms: 77 | services: 78 | metrics: true 79 | requests: 80 | data: data 81 | swagger: 82 | path: "" 83 | file: "swagger.yaml" 84 | tracer: 85 | client: "jaeger" 86 | host: "localhost" 87 | component_name: "Python Microservice" 88 | config: 89 | DEBUG: true 90 | TESTING: true""" 91 | ) 92 | wrong_keywords = [i for i in config.pyms if i not in PYMS_CONFIG_WHITELIST_KEYWORDS] 93 | if len(wrong_keywords) > 0: 94 | raise ConfigErrorException( 95 | """{} isn`t a valid keyword for pyms block, for example: 96 | pyms: 97 | services: 98 | metrics: true 99 | requests: 100 | data: data 101 | swagger: 102 | path: "" 103 | file: "swagger.yaml" 104 | tracer: 105 | client: "jaeger" 106 | host: "localhost" 107 | component_name: "Python Microservice" 108 | config: 109 | DEBUG: true 110 | TESTING: true""".format( 111 | wrong_keywords 112 | ) 113 | ) 114 | 115 | # TODO Remove temporally deprecated warnings on future versions 116 | __verify_deprecated_env_variables(config) 117 | 118 | 119 | def __verify_deprecated_env_variables(config): 120 | env_var_duplicated = 'IMPORTANT: If you are using "{}" environment variable, "{}" value will be ignored.' 121 | env_var_deprecated = 'IMPORTANT: "{}" environment variable is deprecated on this version, use "{}" instead.' 122 | 123 | if os.getenv(CONFIGMAP_FILE_ENVIRONMENT_LEGACY) is not None: 124 | if os.getenv(CONFIGMAP_FILE_ENVIRONMENT) is not None: 125 | msg = env_var_duplicated.format(CONFIGMAP_FILE_ENVIRONMENT, CONFIGMAP_FILE_ENVIRONMENT_LEGACY) 126 | else: 127 | msg = env_var_deprecated.format(CONFIGMAP_FILE_ENVIRONMENT_LEGACY, CONFIGMAP_FILE_ENVIRONMENT) 128 | try: 129 | if config.pyms.config.DEBUG: 130 | msg = utils.colored_text(msg, utils.Colors.BRIGHT_YELLOW, True) 131 | except AttrDoesNotExistException: 132 | pass 133 | logger.warning(msg) 134 | 135 | if os.getenv(CRYPT_FILE_KEY_ENVIRONMENT_LEGACY) is not None: 136 | if os.getenv(CRYPT_FILE_KEY_ENVIRONMENT) is not None: 137 | msg = env_var_duplicated.format(CRYPT_FILE_KEY_ENVIRONMENT, CRYPT_FILE_KEY_ENVIRONMENT_LEGACY) 138 | else: 139 | msg = env_var_deprecated.format(CRYPT_FILE_KEY_ENVIRONMENT_LEGACY, CRYPT_FILE_KEY_ENVIRONMENT) 140 | try: 141 | if config.pyms.config.DEBUG: 142 | msg = utils.colored_text(msg, utils.Colors.BRIGHT_YELLOW, True) 143 | except AttrDoesNotExistException: 144 | pass 145 | logger.warning(msg) 146 | 147 | 148 | def create_conf_file(use_requests: bool = False, use_swagger: bool = False) -> Union[Exception, str]: 149 | """ 150 | Creates a configuration file defining 151 | 152 | :param use_requests: Do you want to use requests, defaults to False 153 | :type use_requests: bool, optional 154 | :param use_swagger: Do you want to use swagger, defaults to False 155 | :type use_swagger: bool, optional 156 | :raises FileExistsError: Config file already exists 157 | :raises IOError: Config file creation failed. 158 | :return: Raises FileExistsError or IOError OR returns config_file_path 159 | :rtype: Union[Exception, str] 160 | """ 161 | # Try using env value for config file, if not found use default 162 | CONFIG_FILE = os.getenv(CONFIGMAP_FILE_ENVIRONMENT, None) 163 | if not CONFIG_FILE: 164 | CONFIG_FILE = DEFAULT_CONFIGMAP_FILENAME 165 | # Prevent overwriting existing file 166 | if os.path.exists(CONFIG_FILE): 167 | raise FileExistsError("Config file already exists at '{}'".format(os.path.abspath(CONFIG_FILE))) 168 | # Create config dict 169 | config = {"pyms": {}} 170 | # add services 171 | if use_requests: 172 | if not config["pyms"].get("services", None): 173 | config["pyms"]["services"] = {} 174 | config["pyms"]["services"]["requests"] = {"data": ""} 175 | if use_swagger: 176 | if not config["pyms"].get("services", None): 177 | config["pyms"]["services"] = {} 178 | config["pyms"]["services"]["swagger"] = {"path": "", "file": "swagger.yaml"} 179 | # add Basic Flask config 180 | config["pyms"]["config"] = { 181 | "DEBUG": True, 182 | "TESTING": False, 183 | "APP_NAME": "Python Microservice", 184 | "APPLICATION_ROOT": "", 185 | } 186 | try: 187 | with open(CONFIG_FILE, "w", encoding="utf-8") as config_file: 188 | config_file.write(yaml.dump(config, default_flow_style=False, default_style=None, sort_keys=False)) 189 | except Exception as ex: 190 | raise ex 191 | return CONFIG_FILE 192 | -------------------------------------------------------------------------------- /pyms/config/confile.py: -------------------------------------------------------------------------------- 1 | """Module to read yaml or json conf""" 2 | 3 | import logging 4 | import os 5 | import re 6 | from typing import Dict, Iterable, Text, Tuple, Union 7 | 8 | import anyconfig 9 | 10 | from pyms.constants import ( 11 | CONFIGMAP_FILE_ENVIRONMENT, 12 | CONFIGMAP_FILE_ENVIRONMENT_LEGACY, 13 | DEFAULT_CONFIGMAP_FILENAME, 14 | LOGGER_NAME, 15 | ) 16 | from pyms.exceptions import AttrDoesNotExistException, ConfigDoesNotFoundException 17 | from pyms.utils.files import LoadFile 18 | 19 | logger = logging.getLogger(LOGGER_NAME) 20 | 21 | 22 | class ConfFile(dict): 23 | """Recursive get configuration from dictionary, a config file in JSON or YAML format from a path or 24 | `PYMS_CONFIGMAP_FILE` environment variable. 25 | **Atributes:** 26 | * path: Path to find the `DEFAULT_CONFIGMAP_FILENAME` and `DEFAULT_KEY_FILENAME` if use encrypted vars 27 | * empty_init: Allow blank variables 28 | * config: Allow to pass a dictionary to ConfFile without use a file 29 | """ 30 | 31 | _empty_init = False 32 | _crypt = None 33 | 34 | def __init__(self, *args, **kwargs): 35 | """ 36 | Get configuration from a dictionary(variable `config`), from path (variable `path`) or from 37 | environment with the constant `PYMS_CONFIGMAP_FILE` 38 | Set the configuration as upper case to inject the keys in flask config. Flask search for uppercase keys in 39 | `app.config.from_object` 40 | ```python 41 | if key.isupper(): 42 | self[key] = getattr(obj, key) 43 | ``` 44 | """ 45 | # TODO Remove temporally backward compatibility on future versions 46 | configmap_file_env = self.__get_updated_configmap_file_env() # Temporally backward compatibility 47 | 48 | self._loader = LoadFile(kwargs.get("path"), configmap_file_env, DEFAULT_CONFIGMAP_FILENAME) 49 | self._crypt_cls = kwargs.get("crypt") 50 | if self._crypt_cls: 51 | self._crypt = self._crypt_cls(path=kwargs.get("path")) 52 | self._empty_init = kwargs.get("empty_init", False) 53 | config = kwargs.get("config") 54 | if config is None: 55 | config = self._loader.get_file(anyconfig.load) 56 | if not config: 57 | if self._empty_init: 58 | config = {} 59 | else: 60 | path = self._loader.path if self._loader.path else "" 61 | raise ConfigDoesNotFoundException("Configuration file {}not found".format(path + " ")) 62 | 63 | config = self.set_config(config) 64 | 65 | super().__init__(config) 66 | 67 | def to_flask(self) -> Dict: 68 | return ConfFile(config={k.upper(): v for k, v in self.items()}, crypt=self._crypt_cls) 69 | 70 | def set_config(self, config: Dict) -> Dict: 71 | """ 72 | Set a dictionary as attributes of ConfFile. This attributes could be access as `ConfFile["attr"]` or 73 | ConfFile.attr 74 | :param config: a dictionary from `config.yml` 75 | :return: 76 | """ 77 | config = dict(self.normalize_config(config)) 78 | pop_encripted_keys = [] 79 | add_decripted_keys = [] 80 | for k, v in config.items(): 81 | if k.lower().startswith("enc_"): 82 | k_not_crypt = re.compile(re.escape("enc_"), re.IGNORECASE) 83 | decrypted_key = k_not_crypt.sub("", k) 84 | decrypted_value = self._crypt.decrypt(v) if self._crypt else None 85 | setattr(self, decrypted_key, decrypted_value) 86 | add_decripted_keys.append((decrypted_key, decrypted_value)) 87 | pop_encripted_keys.append(k) 88 | else: 89 | setattr(self, k, v) 90 | 91 | # Delete encrypted keys to prevent decrypt multiple times a element 92 | for x in pop_encripted_keys: 93 | config.pop(x) 94 | 95 | for k, v in add_decripted_keys: 96 | config[k] = v 97 | 98 | return config 99 | 100 | def normalize_config(self, config: Dict) -> Iterable[Tuple[Text, Union[Dict, Text, bool]]]: 101 | for key, item in config.items(): 102 | if isinstance(item, dict): 103 | item = ConfFile(config=item, empty_init=self._empty_init, crypt=self._crypt_cls) 104 | yield self.normalize_keys(key), item 105 | 106 | @staticmethod 107 | def normalize_keys(key: Text) -> Text: 108 | """The keys will be transformed to a attribute. We need to replace the charactes not valid""" 109 | key = key.replace("-", "_") 110 | return key 111 | 112 | def __eq__(self, other): 113 | if not isinstance(other, ConfFile) and not isinstance(other, dict): 114 | return False 115 | return dict(self) == dict(other) 116 | 117 | def __getattr__(self, name, *args, **kwargs): 118 | try: 119 | keys = self.normalize_keys(name).split(".") 120 | aux_dict = self 121 | for k in keys: 122 | aux_dict = aux_dict[k] 123 | return aux_dict 124 | except KeyError as e: 125 | if self._empty_init: 126 | return ConfFile(config={}, empty_init=self._empty_init, crypt=self._crypt_cls) 127 | raise AttrDoesNotExistException("Variable {} not exist in the config file".format(name)) from e 128 | 129 | def reload(self): 130 | """ 131 | Remove file from memoize variable, return again the content of the file and set the configuration again 132 | :return: None 133 | """ 134 | config_src = self._loader.reload(anyconfig.load) 135 | self.set_config(config_src) 136 | 137 | def __setattr__(self, name, value, *args, **kwargs): 138 | super().__setattr__(name, value) 139 | 140 | @staticmethod 141 | def __get_updated_configmap_file_env() -> str: 142 | result = CONFIGMAP_FILE_ENVIRONMENT 143 | if (os.getenv(CONFIGMAP_FILE_ENVIRONMENT_LEGACY) is not None) and ( 144 | os.getenv(CONFIGMAP_FILE_ENVIRONMENT) is None 145 | ): 146 | result = CONFIGMAP_FILE_ENVIRONMENT_LEGACY 147 | return result 148 | -------------------------------------------------------------------------------- /pyms/config/resource.py: -------------------------------------------------------------------------------- 1 | from typing import Text 2 | 3 | from pyms.config import get_conf 4 | 5 | 6 | class ConfigResource: 7 | 8 | config_resource: Text = "" 9 | 10 | def __init__(self, *args, **kwargs): 11 | self.config = get_conf(service=self.config_resource, empty_init=True, uppercase=False, *args, **kwargs) 12 | -------------------------------------------------------------------------------- /pyms/constants.py: -------------------------------------------------------------------------------- 1 | CONFIGMAP_FILE_ENVIRONMENT = "PYMS_CONFIGMAP_FILE" 2 | CONFIGMAP_FILE_ENVIRONMENT_LEGACY = "CONFIGMAP_FILE" 3 | 4 | DEFAULT_CONFIGMAP_FILENAME = "config.yml" 5 | 6 | CRYPT_FILE_KEY_ENVIRONMENT = "PYMS_KEY_FILE" 7 | CRYPT_FILE_KEY_ENVIRONMENT_LEGACY = "KEY_FILE" 8 | 9 | DEFAULT_KEY_FILENAME = "key.key" 10 | 11 | LOGGER_NAME = "pyms" 12 | 13 | CONFIG_BASE = "pyms.config" 14 | 15 | SERVICE_BASE = "pyms.services" 16 | 17 | CRYPT_BASE = "pyms.crypt" 18 | 19 | PYMS_CONFIG_WHITELIST_KEYWORDS = ["config", "services", "crypt"] 20 | -------------------------------------------------------------------------------- /pyms/crypt/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-microservices/pyms/e7c50a127ffe51f67808600a55fddcde7d24664e/pyms/crypt/__init__.py -------------------------------------------------------------------------------- /pyms/crypt/driver.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from abc import ABC, abstractmethod 3 | 4 | from pyms.config.resource import ConfigResource 5 | from pyms.constants import CRYPT_BASE, LOGGER_NAME 6 | from pyms.utils import import_from 7 | 8 | logger = logging.getLogger(LOGGER_NAME) 9 | 10 | CRYPT_RESOURCES_CLASS = "Crypt" 11 | 12 | 13 | class CryptAbstract(ABC): 14 | def __init__(self, *args, **kwargs): 15 | self.config = kwargs.get("config") 16 | 17 | @abstractmethod 18 | def encrypt(self, message): 19 | raise NotImplementedError 20 | 21 | @abstractmethod 22 | def decrypt(self, encrypted): 23 | raise NotImplementedError 24 | 25 | 26 | class CryptNone(CryptAbstract): 27 | def encrypt(self, message): 28 | return message 29 | 30 | def decrypt(self, encrypted): 31 | return encrypted 32 | 33 | 34 | class CryptResource(ConfigResource): 35 | """This class works between `pyms.flask.create_app.Microservice` and `pyms.flask.services.[THESERVICE]`. Search 36 | for a file with the name you want to load, set the configuration and return a instance of the class you want 37 | """ 38 | 39 | config_resource = CRYPT_BASE 40 | 41 | def get_crypt(self, *args, **kwargs) -> CryptAbstract: 42 | if self.config.method == "fernet": 43 | crypt_object = import_from("pyms.crypt.fernet", CRYPT_RESOURCES_CLASS) 44 | elif self.config.method == "aws_kms": 45 | crypt_object = import_from("pyms.cloud.aws.kms", CRYPT_RESOURCES_CLASS) 46 | else: 47 | crypt_object = CryptNone 48 | logger.debug("Init crypt {}".format(crypt_object)) 49 | return crypt_object(config=self.config, *args, **kwargs) 50 | 51 | def __call__(self, *args, **kwargs): 52 | return self.get_crypt(*args, **kwargs) 53 | -------------------------------------------------------------------------------- /pyms/crypt/fernet.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import os 3 | from typing import Text 4 | 5 | from cryptography.fernet import Fernet 6 | from cryptography.hazmat.backends import default_backend 7 | from cryptography.hazmat.primitives import hashes 8 | from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC 9 | 10 | from pyms.constants import CRYPT_FILE_KEY_ENVIRONMENT, CRYPT_FILE_KEY_ENVIRONMENT_LEGACY, DEFAULT_KEY_FILENAME 11 | from pyms.crypt.driver import CryptAbstract 12 | from pyms.exceptions import FileDoesNotExistException 13 | from pyms.utils.files import LoadFile 14 | 15 | 16 | class Crypt(CryptAbstract): 17 | def __init__(self, *args, **kwargs): 18 | # TODO Remove temporally backward compatibility on future versions 19 | crypt_file_key_env = self.__get_updated_crypt_file_key_env() # Temporally backward compatibility 20 | self._loader = LoadFile(kwargs.get("path"), crypt_file_key_env, DEFAULT_KEY_FILENAME) 21 | super().__init__(*args, **kwargs) 22 | 23 | def generate_key(self, password: Text, write_to_file: bool = False) -> bytes: 24 | byte_password = password.encode() # Convert to type bytes 25 | salt = os.urandom(16) 26 | kdf = PBKDF2HMAC( 27 | algorithm=hashes.SHA512_256(), length=32, salt=salt, iterations=100000, backend=default_backend() 28 | ) 29 | key = base64.urlsafe_b64encode(kdf.derive(byte_password)) # Can only use kdf once 30 | if write_to_file: 31 | self._loader.put_file(key, "wb") 32 | return key 33 | 34 | def read_key(self): 35 | key = self._loader.get_file() 36 | if not key: 37 | # TODO Remove temporally backward compatibility on future versions 38 | crypt_file_key_env = self.__get_updated_crypt_file_key_env() # Temporally backward compatibility 39 | raise FileDoesNotExistException( 40 | "Decrypt key {} not exists. You must set a correct env var {} " 41 | "or run `pyms crypt create-key` command".format(self._loader.path, crypt_file_key_env) 42 | ) 43 | return key 44 | 45 | def encrypt(self, message): 46 | key = self.read_key() 47 | message = message.encode() 48 | f = Fernet(key) 49 | encrypted = f.encrypt(message) 50 | return encrypted 51 | 52 | def decrypt(self, encrypted): 53 | key = self.read_key() 54 | encrypted = encrypted.encode() 55 | f = Fernet(key) 56 | decrypted = f.decrypt(encrypted) 57 | return str(decrypted, encoding="utf-8") 58 | 59 | def delete_key(self): 60 | os.remove(self._loader.get_path_from_env()) 61 | 62 | @staticmethod 63 | def __get_updated_crypt_file_key_env() -> str: 64 | result = CRYPT_FILE_KEY_ENVIRONMENT 65 | if (os.getenv(CRYPT_FILE_KEY_ENVIRONMENT_LEGACY) is not None) and ( 66 | os.getenv(CRYPT_FILE_KEY_ENVIRONMENT) is None 67 | ): 68 | result = CRYPT_FILE_KEY_ENVIRONMENT_LEGACY 69 | return result 70 | -------------------------------------------------------------------------------- /pyms/exceptions.py: -------------------------------------------------------------------------------- 1 | """Exceptions of the lib. Its useful """ 2 | 3 | 4 | class AttrDoesNotExistException(Exception): 5 | pass 6 | 7 | 8 | class FileDoesNotExistException(Exception): 9 | pass 10 | 11 | 12 | class ServiceDoesNotExistException(Exception): 13 | pass 14 | 15 | 16 | class ConfigDoesNotFoundException(Exception): 17 | pass 18 | 19 | 20 | class ConfigErrorException(Exception): 21 | pass 22 | 23 | 24 | class PackageNotExists(Exception): 25 | pass 26 | 27 | 28 | class ServiceDiscoveryConnectionException(Exception): 29 | pass 30 | -------------------------------------------------------------------------------- /pyms/flask/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-microservices/pyms/e7c50a127ffe51f67808600a55fddcde7d24664e/pyms/flask/__init__.py -------------------------------------------------------------------------------- /pyms/flask/app/__init__.py: -------------------------------------------------------------------------------- 1 | from .create_app import Microservice 2 | from .create_config import config 3 | 4 | __all__ = ["Microservice", "config"] 5 | -------------------------------------------------------------------------------- /pyms/flask/app/create_app.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | from typing import List, Optional 4 | 5 | from flask import Flask 6 | 7 | from pyms.config.conf import validate_conf 8 | from pyms.config.resource import ConfigResource 9 | from pyms.constants import CONFIG_BASE, LOGGER_NAME 10 | from pyms.crypt.driver import CryptResource 11 | from pyms.flask.app.utils import SingletonMeta 12 | from pyms.flask.configreload import configreload_blueprint 13 | from pyms.flask.healthcheck import healthcheck_blueprint 14 | from pyms.flask.services.driver import DriverService, ServicesResource 15 | from pyms.logger import CustomJsonFormatter 16 | from pyms.utils import check_package_exists 17 | 18 | logger = logging.getLogger(LOGGER_NAME) 19 | 20 | 21 | class Microservice(ConfigResource, metaclass=SingletonMeta): 22 | """The class Microservice is the core of all microservices built with PyMS. 23 | See this docs: https://python-microservices.github.io/ms_class/ 24 | """ 25 | 26 | config_resource = CONFIG_BASE 27 | services: List[str] = [] 28 | application = Flask 29 | swagger: Optional[DriverService] = None 30 | request: Optional[DriverService] = None 31 | tracer: Optional[DriverService] = None 32 | metrics: Optional[DriverService] = None 33 | _singleton = True 34 | 35 | def __init__(self, *args, **kwargs): 36 | """ 37 | You can get the relative path from the current directory with `__file__` in path param. The path must be 38 | the folder where PyMS search for default config file, default swagger definition and encrypt key. 39 | :param args: 40 | :param kwargs: "path", optional, the current directory where `Microservice` class is instanciated 41 | """ 42 | path = kwargs.pop("path") if kwargs.get("path") else None 43 | self.path = os.path.abspath("") 44 | if path: 45 | self.path = os.path.dirname(os.path.abspath(path)) 46 | 47 | validate_conf() 48 | self.init_crypt(path=self.path, *args, **kwargs) 49 | super().__init__(path=self.path, crypt=self.crypt, *args, **kwargs) 50 | self.init_services() 51 | 52 | def init_services(self) -> None: 53 | """ 54 | Set the Attributes of all service defined in config.yml and exists in `pyms.flask.service` module 55 | :return: None 56 | """ 57 | services_resources = ServicesResource() 58 | for service_name, service in services_resources.get_services(): 59 | if service_name not in self.services or not getattr(self, service_name, False): 60 | self.services.append(service_name) 61 | setattr(self, service_name, service) 62 | 63 | def init_services_actions(self): 64 | for service_name in self.services: 65 | srv_action = getattr(getattr(self, service_name), "init_action") 66 | if srv_action: 67 | srv_action(self) 68 | 69 | def init_crypt(self, *args, **kwargs) -> None: 70 | """ 71 | Set the Attributes of all service defined in config.yml and exists in `pyms.flask.service` module 72 | :return: None 73 | """ 74 | crypt_object = CryptResource(*args, **kwargs) 75 | self.crypt = crypt_object 76 | 77 | def delete_services(self) -> None: 78 | """ 79 | Set the Attributes of all service defined in config.yml and exists in `pyms.flask.service` module 80 | :return: None 81 | """ 82 | for service_name in self.services: 83 | try: 84 | delattr(self, service_name) 85 | except AttributeError: 86 | pass 87 | 88 | def init_libs(self) -> Flask: 89 | """This function exists to override if you need to set more libs such as SQLAlchemy, CORs, and any else 90 | library needs to be init over flask, like the usual pattern [MYLIB].init_app(app) 91 | :return: 92 | """ 93 | return self.application 94 | 95 | def init_logger(self) -> None: 96 | """ 97 | Set a logger and return in JSON format. 98 | :return: 99 | """ 100 | self.application.logger = logger 101 | 102 | formatter = CustomJsonFormatter() 103 | formatter.add_service_name(self.application.config.get("APP_NAME", "no_service_name")) 104 | log_handler = logging.StreamHandler() 105 | log_handler.setFormatter(formatter) 106 | 107 | self.application.logger.addHandler(log_handler) 108 | 109 | self.application.logger.propagate = False 110 | 111 | if self.application.config["DEBUG"]: 112 | self.application.logger.setLevel(logging.DEBUG) 113 | else: # pragma: no cover 114 | self.application.logger.setLevel(logging.INFO) 115 | 116 | def init_app(self) -> Flask: 117 | """Set attribute in flask `swagger`. See in `pyms.flask.services.swagger` how it works. If not set, 118 | run a "normal" Flask app. 119 | :return: None 120 | """ 121 | if self.swagger: 122 | application = self.swagger.init_app(config=self.config.to_flask(), path=self.path) 123 | else: 124 | check_package_exists("flask") 125 | application = Flask( 126 | __name__, 127 | static_folder=os.path.join(self.path, "static"), 128 | template_folder=os.path.join(self.path, "templates"), 129 | ) 130 | 131 | application.root_path = self.path 132 | 133 | # Fix connexion issue https://github.com/spec-first/connexion/issues/527 134 | # application.wsgi_app = ReverseProxied(application.wsgi_app) 135 | 136 | return application 137 | 138 | def reload_conf(self): 139 | self.delete_services() 140 | self.config.reload() 141 | self.services = [] 142 | self.init_services() 143 | self.crypt.config.reload() 144 | self.create_app() 145 | 146 | def create_app(self) -> Flask: 147 | """Initialize the Flask app, register blueprints and initialize 148 | all libraries like Swagger, database, 149 | the trace system... 150 | return the app and the database objects. 151 | :return: 152 | """ 153 | self.application = self.init_app() 154 | if hasattr(self.application, "connexion_app"): 155 | self.application.connexion_app.app.config.from_object(self.config.to_flask()) 156 | self.application.ms = self 157 | 158 | # Initialize Blueprints 159 | self.application.connexion_app.app.register_blueprint(healthcheck_blueprint) 160 | self.application.connexion_app.app.register_blueprint(configreload_blueprint) 161 | 162 | else: 163 | self.application.config.from_object(self.config.to_flask()) 164 | self.application.ms = self 165 | 166 | # Initialize Blueprints 167 | self.application.register_blueprint(healthcheck_blueprint) 168 | self.application.register_blueprint(configreload_blueprint) 169 | 170 | self.init_libs() 171 | self.add_error_handlers() 172 | self.init_logger() 173 | 174 | self.init_services_actions() 175 | 176 | logger.debug("Started app with PyMS and this services: {}".format(self.services)) 177 | 178 | return self.application 179 | 180 | def add_error_handlers(self) -> None: 181 | """Subclasses will override this method in order to add specific error handlers. This should be done with 182 | calls to add_error_handler method. 183 | """ 184 | pass 185 | 186 | def add_error_handler(self, code_or_exception, handler) -> None: 187 | """Add custom handler for an error code or exception in the connexion app. 188 | 189 | :param code_or_exception: HTTP error code or exception 190 | :param handler: callback for error handler 191 | """ 192 | self.application.connexion_app.add_error_handler(code_or_exception, handler) 193 | -------------------------------------------------------------------------------- /pyms/flask/app/create_config.py: -------------------------------------------------------------------------------- 1 | from pyms.flask.app.create_app import Microservice 2 | 3 | 4 | def config(): 5 | """The behavior of this function is to access to the configuration outer the scope of flask context, to prevent to 6 | raise a `'working outside of application context` 7 | :return: 8 | """ 9 | ms = Microservice() 10 | return ms.config 11 | -------------------------------------------------------------------------------- /pyms/flask/app/utils.py: -------------------------------------------------------------------------------- 1 | from typing import Dict 2 | 3 | 4 | class SingletonMeta(type): 5 | """ 6 | The Singleton class can be implemented in different ways in Python. Some 7 | possible methods include: base class, decorator, metaclass. We will use the 8 | metaclass because it is best suited for this purpose. 9 | """ 10 | 11 | _instances: Dict[type, type] = {} 12 | _singleton = True 13 | 14 | def __call__(cls, *args, **kwargs) -> type: 15 | if cls not in cls._instances or not cls._singleton: 16 | cls._instances[cls] = super(SingletonMeta, cls).__call__(*args, **kwargs) 17 | else: 18 | cls._instances[cls].__init__(*args, **kwargs) 19 | 20 | return cls._instances[cls] 21 | 22 | 23 | class ReverseProxied: 24 | """ 25 | Create a Proxy pattern https://microservices.io/patterns/apigateway.html. 26 | You can run the microservice A in your local machine in http://localhost:5000/my-endpoint/ 27 | If you deploy your microservice, in some cases this microservice run behind a cluster, a gateway... and this 28 | gateway redirect traffic to the microservice with a specific path like yourdomian.com/my-ms-a/my-endpoint/. 29 | This class understand this path if the gateway send a specific header 30 | """ 31 | 32 | def __init__(self, app): 33 | self.app = app 34 | 35 | @staticmethod 36 | def _extract_prefix(environ: dict) -> str: 37 | """ 38 | Get Path from environment from: 39 | - Traefik with HTTP_X_SCRIPT_NAME https://docs.traefik.io/v2.0/middlewares/headers/ 40 | - Nginx and Ingress with HTTP_X_SCRIPT_NAME https://www.nginx.com/resources/wiki/start/topics/examples/forwarded/ 41 | - Apache with HTTP_X_SCRIPT_NAME https://stackoverflow.com/questions/55619013/proxy-and-rewrite-to-webapp 42 | - Zuul with HTTP_X_FORWARDER_PREFIX https://cloud.spring.io/spring-cloud-netflix/multi/multi__router_and_filter_zuul.html 43 | :param environ: 44 | :return: 45 | """ 46 | # Get path from Traefik, Nginx and Apache 47 | path = environ.get("HTTP_X_SCRIPT_NAME", "") 48 | if not path: 49 | # Get path from Zuul 50 | path = environ.get("HTTP_X_FORWARDED_PREFIX", "") 51 | if path and not path.startswith("/"): 52 | path = "/" + path 53 | return path 54 | 55 | def __call__(self, environ, start_response): 56 | script_name = self._extract_prefix(environ) 57 | if script_name: 58 | environ["SCRIPT_NAME"] = script_name 59 | path_info = environ["PATH_INFO"] 60 | if path_info.startswith(script_name): 61 | environ["PATH_INFO"] = path_info[len(script_name) :] # noqa: E203 62 | 63 | scheme = environ.get("HTTP_X_SCHEME", "") 64 | if scheme: 65 | environ["wsgi.url_scheme"] = scheme 66 | return self.app(environ, start_response) 67 | -------------------------------------------------------------------------------- /pyms/flask/configreload/__init__.py: -------------------------------------------------------------------------------- 1 | from .configreload import configreload_blueprint 2 | 3 | __all__ = ["configreload_blueprint"] 4 | -------------------------------------------------------------------------------- /pyms/flask/configreload/configreload.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, division, print_function, unicode_literals 2 | 3 | from flask import Blueprint, current_app 4 | 5 | configreload_blueprint = Blueprint("configreload", __name__, static_url_path="/static") 6 | 7 | 8 | @configreload_blueprint.route("/reload-config", methods=["POST"]) 9 | def reloadconfig(): 10 | """ 11 | Reread configuration from file. 12 | :return: 13 | """ 14 | current_app.ms.reload_conf() 15 | return "OK" 16 | -------------------------------------------------------------------------------- /pyms/flask/healthcheck/__init__.py: -------------------------------------------------------------------------------- 1 | from .healthcheck import healthcheck_blueprint 2 | 3 | __all__ = ["healthcheck_blueprint"] 4 | -------------------------------------------------------------------------------- /pyms/flask/healthcheck/healthcheck.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, division, print_function, unicode_literals 2 | 3 | from flask import Blueprint 4 | 5 | healthcheck_blueprint = Blueprint("healthcheck", __name__, static_url_path="/static") 6 | 7 | 8 | @healthcheck_blueprint.route("/healthcheck", methods=["GET"]) 9 | def healthcheck(): 10 | """Set a healthcheck to help other service to discover this microservice, like Kubernetes, AWS ELB, etc. 11 | :return: 12 | """ 13 | return "OK" 14 | -------------------------------------------------------------------------------- /pyms/flask/services/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-microservices/pyms/e7c50a127ffe51f67808600a55fddcde7d24664e/pyms/flask/services/__init__.py -------------------------------------------------------------------------------- /pyms/flask/services/driver.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from typing import Iterator, Text, Tuple 3 | 4 | from pyms.config import ConfFile 5 | from pyms.config.resource import ConfigResource 6 | from pyms.constants import LOGGER_NAME, SERVICE_BASE 7 | from pyms.utils import import_from 8 | 9 | logger = logging.getLogger(LOGGER_NAME) 10 | 11 | 12 | def get_service_name(service_base: str = SERVICE_BASE, service: str = "") -> str: 13 | return ".".join([service_base, service]) 14 | 15 | 16 | class DriverService(ConfigResource): 17 | """All services must inherit from this class. This set the configuration. If we have got his config file: 18 | See these docs: 19 | * https://python-microservices.github.io/configuration/ 20 | * https://python-microservices.github.io/services/ 21 | """ 22 | 23 | enabled = True 24 | 25 | init_action = False 26 | 27 | def __init__(self, *args, **kwargs): 28 | self.config_resource = get_service_name(service=self.config_resource) 29 | super().__init__(*args, **kwargs) 30 | 31 | def __getattr__(self, attr, *args, **kwargs): 32 | config_attribute = getattr(self.config, attr) 33 | return ( 34 | config_attribute 35 | if config_attribute == "" or config_attribute != {} 36 | else self.default_values.get(attr, None) 37 | ) 38 | 39 | def is_enabled(self) -> bool: 40 | return self.enabled 41 | 42 | def exists_config(self) -> bool: 43 | return self.config is not None and isinstance(self.config, ConfFile) 44 | 45 | 46 | class ServicesResource(ConfigResource): 47 | """This class works between `pyms.flask.create_app.Microservice` and `pyms.flask.services.[THESERVICE]`. Search 48 | for a file with the name you want to load, set the configuration and return a instance of the class you want 49 | See these docs: 50 | * https://python-microservices.github.io/configuration/ 51 | * https://python-microservices.github.io/services/ 52 | """ 53 | 54 | config_resource = SERVICE_BASE 55 | 56 | def get_services(self) -> Iterator[Tuple[Text, DriverService]]: 57 | for k in self.config.__dict__.keys(): 58 | if k.islower() and not k.startswith("_"): 59 | service = self.get_service(k) 60 | if service.is_enabled(): 61 | yield k, service 62 | 63 | @staticmethod 64 | def get_service(service: Text, *args, **kwargs) -> DriverService: 65 | service_object = import_from("pyms.flask.services.{}".format(service), "Service") 66 | logger.debug("Init service {}".format(service)) 67 | return service_object(*args, **kwargs) 68 | -------------------------------------------------------------------------------- /pyms/flask/services/metrics.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import time 3 | 4 | from flask import Blueprint, Flask, Response, request 5 | from prometheus_client import REGISTRY, CollectorRegistry, Counter, Histogram, generate_latest, multiprocess 6 | 7 | from pyms.config.conf import get_conf 8 | from pyms.flask.services.driver import DriverService, get_service_name 9 | 10 | # Based on https://github.com/sbarratt/flask-prometheus 11 | # and https://github.com/korfuri/python-logging-prometheus/ 12 | 13 | METRICS_CONFIG = get_conf(service=get_service_name(service="metrics"), empty_init=True) 14 | 15 | FLASK_REQUEST_COUNT = Counter( 16 | "http_server_requests_count", "Flask Request Count", ["service", "method", "uri", "status"] 17 | ) 18 | 19 | FLASK_REQUEST_LATENCY = Histogram( 20 | "http_server_requests_seconds", "Flask Request Latency", ["service", "method", "uri", "status"] 21 | ) 22 | 23 | LOGGER_TOTAL_MESSAGES = Counter( 24 | "logger_messages_total", 25 | "Count of log entries by service and level.", 26 | ["service", "level"], 27 | ) 28 | 29 | 30 | class FlaskMetricsWrapper: 31 | def __init__(self, app_name): 32 | self.app_name = app_name 33 | 34 | def before_request(self) -> None: 35 | request.start_time = time.time() # pylint: disable=assigning-non-slot 36 | 37 | def after_request(self, response: Response) -> Response: 38 | if hasattr(request.url_rule, "rule"): 39 | path = request.url_rule.rule 40 | else: 41 | path = request.path 42 | request_latency = time.time() - request.start_time 43 | FLASK_REQUEST_COUNT.labels(self.app_name, request.method, path, response.status_code).inc() 44 | FLASK_REQUEST_LATENCY.labels(self.app_name, request.method, path, response.status_code).observe(request_latency) 45 | 46 | return response 47 | 48 | 49 | class Service(DriverService): 50 | """ 51 | Adds [Prometheus](https://prometheus.io/) metrics using the [Prometheus Client Library](https://github.com/prometheus/client_python). 52 | """ 53 | 54 | def __init__(self, *args, **kwargs): 55 | super().__init__(*args, **kwargs) 56 | self.metrics_blueprint = Blueprint("metrics", __name__) 57 | self.init_registry() 58 | self.serve_metrics() 59 | 60 | def init_action(self, microservice_instance): 61 | microservice_instance.application.register_blueprint(microservice_instance.metrics.metrics_blueprint) 62 | self.add_logger_handler( 63 | microservice_instance.application.logger, microservice_instance.application.config["APP_NAME"] 64 | ) 65 | self.monitor(microservice_instance.application.config["APP_NAME"], microservice_instance.application) 66 | 67 | def init_registry(self) -> None: 68 | try: 69 | multiprocess_registry = CollectorRegistry() 70 | multiprocess.MultiProcessCollector(multiprocess_registry) 71 | self.registry = multiprocess_registry 72 | except ValueError: 73 | self.registry = REGISTRY 74 | 75 | @staticmethod 76 | def monitor(app_name: str, app: Flask) -> None: 77 | metric = FlaskMetricsWrapper(app_name) 78 | app.before_request(metric.before_request) 79 | app.after_request(metric.after_request) 80 | 81 | def serve_metrics(self): 82 | @self.metrics_blueprint.route("/metrics", methods=["GET"]) 83 | def metrics(): # pylint: disable=unused-variable 84 | return Response( 85 | generate_latest(self.registry), 86 | mimetype="text/print()lain", 87 | content_type="text/plain; charset=utf-8", 88 | ) 89 | 90 | @staticmethod 91 | def add_logger_handler(logger: logging.Logger, service_name: str) -> logging.Logger: 92 | logger.addHandler(MetricsLogHandler(service_name)) 93 | return logger 94 | 95 | 96 | class MetricsLogHandler(logging.Handler): 97 | """A LogHandler that exports logging metrics for Prometheus.io.""" 98 | 99 | def __init__(self, app_name): 100 | super().__init__() 101 | self.app_name = app_name 102 | 103 | def emit(self, record) -> None: 104 | LOGGER_TOTAL_MESSAGES.labels(self.app_name, record.levelname).inc() 105 | -------------------------------------------------------------------------------- /pyms/flask/services/service_discovery.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | try: 4 | import consulate 5 | except ModuleNotFoundError: # pragma: no cover 6 | consulate = None 7 | 8 | from pyms.constants import LOGGER_NAME 9 | from pyms.flask.services.driver import DriverService 10 | from pyms.utils.utils import import_class 11 | 12 | logger = logging.getLogger(LOGGER_NAME) 13 | 14 | CONSUL_SERVICE_DISCOVERY = "consul" 15 | 16 | DEFAULT_SERVICE_DISCOVERY = CONSUL_SERVICE_DISCOVERY 17 | 18 | 19 | class ServiceDiscoveryBase: 20 | client = None 21 | 22 | def __init__(self, config): 23 | pass 24 | 25 | def register_service(self, *args, **kwargs): 26 | pass 27 | 28 | 29 | class ServiceDiscoveryConsul(ServiceDiscoveryBase): 30 | def __init__(self, config): 31 | super().__init__(config) 32 | self.client = consulate.Consul( 33 | host=config.host, port=config.port, token=config.token, scheme=config.scheme, adapter=config.adapter 34 | ) 35 | 36 | def register_service(self, *args, **kwargs): 37 | self.client.agent.check.register( 38 | kwargs["app_name"], http=kwargs["healtcheck_url"], interval=kwargs.get("interval", "10s") 39 | ) 40 | 41 | 42 | class Service(DriverService): 43 | config_resource = "service_discovery" 44 | default_values = { 45 | "service": DEFAULT_SERVICE_DISCOVERY, 46 | "host": "localhost", 47 | "scheme": "http", 48 | "port": 8500, 49 | "healtcheck_url": "http://127.0.0.1.nip.io:5000/healthcheck", 50 | "interval": "10s", 51 | "autoregister": False, 52 | } 53 | 54 | def init_action(self, microservice_instance): 55 | if self.autoregister: 56 | app_name = microservice_instance.application.config["APP_NAME"] 57 | self._client.register_service(healtcheck_url=self.healtcheck_url, app_name=app_name, interval=self.interval) 58 | 59 | def __init__(self, *args, **kwargs): 60 | super().__init__(*args, **kwargs) 61 | self._client = self.get_client() 62 | self.client = self._client.client 63 | 64 | def get_client(self) -> ServiceDiscoveryBase: 65 | if self.service == CONSUL_SERVICE_DISCOVERY: 66 | client = ServiceDiscoveryConsul(self) 67 | else: 68 | client = import_class(self.service)(self) 69 | 70 | logger.debug("Init %s as service discovery", client) 71 | return client 72 | -------------------------------------------------------------------------------- /pyms/flask/services/swagger.py: -------------------------------------------------------------------------------- 1 | import os 2 | from pathlib import Path 3 | from typing import Any, Dict 4 | 5 | import connexion 6 | from connexion.options import SwaggerUIOptions 7 | from connexion.resolver import RestyResolver 8 | from flask import Flask 9 | 10 | try: 11 | import prance 12 | from prance.util import formats, fs 13 | except ModuleNotFoundError: # pragma: no cover 14 | prance = None 15 | 16 | from pyms.exceptions import AttrDoesNotExistException 17 | from pyms.flask.services.driver import DriverService 18 | from pyms.utils.utils import check_package_exists, import_class 19 | 20 | SWAGGER_PATH = "swagger" 21 | SWAGGER_FILE = "swagger.yaml" 22 | SWAGGER_URL = "ui/" 23 | PROJECT_DIR = "project" 24 | 25 | 26 | def get_bundled_specs(main_file: Path) -> Dict[str, Any]: 27 | """ 28 | Get bundled specs 29 | :param main_file: Swagger file path 30 | :return: 31 | """ 32 | parser = prance.ResolvingParser(str(main_file.absolute()), lazy=True, backend="openapi-spec-validator") 33 | parser.parse() 34 | return parser.specification 35 | 36 | 37 | def merge_swagger_file(main_file: str) -> None: 38 | """ 39 | Generate swagger into a single file 40 | :param main_file: Swagger file path 41 | :return: 42 | """ 43 | input_file = Path(main_file) 44 | output_file = Path(input_file.parent, "swagger-complete.yaml") 45 | 46 | contents = formats.serialize_spec( 47 | specs=get_bundled_specs(input_file), 48 | filename=output_file, 49 | ) 50 | fs.write_file(filename=output_file, contents=contents, encoding="utf-8") 51 | 52 | 53 | class Service(DriverService): 54 | """The parameters you can add to your config are: 55 | * **path:** The relative or absolute route to your swagger yaml file. The default value is the current directory 56 | * **file:** The name of you swagger yaml file. The default value is `swagger.yaml` 57 | * **url:** The url where swagger run in your server. The default value is `/ui/`. 58 | * **project_dir:** Relative path of the project folder to automatic routing, 59 | see [this link for more info](https://github.com/spec-first/connexion#automatic-routing). 60 | The default value is `project` 61 | 62 | All default values keys are created as class attributes in `DriverService` 63 | """ 64 | 65 | config_resource = "swagger" 66 | default_values = { 67 | "path": SWAGGER_PATH, 68 | "file": SWAGGER_FILE, 69 | "url": SWAGGER_URL, 70 | "project_dir": PROJECT_DIR, 71 | "validator_map": {}, 72 | "validate_responses": True, 73 | } 74 | 75 | @staticmethod 76 | def _get_application_root(config) -> str: 77 | try: 78 | application_root = config.APPLICATION_ROOT 79 | except AttrDoesNotExistException: 80 | application_root = "/" 81 | return application_root 82 | 83 | def init_app(self, config, path: Path) -> Flask: 84 | """ 85 | Initialize Connexion App. See more info in [Connexion Github](https://github.com/spec-first/connexion) 86 | :param config: The Flask configuration defined in the config.yaml: 87 | ```yaml 88 | pyms: 89 | services: 90 | requests: true 91 | swagger: 92 | path: "" 93 | file: "swagger.yaml" 94 | config: