Example of an API built with Python3, followin Hexagonal architecture (ports & adapters) and SOLID principles.
It uses some well known packages as Flask and SQLAlchemy.
Available endpoints:
-
/acquire/pdf
- input: PDF file, by
POST
, as form-data. Expected POST param name is:file
. - output: JSON like
{"plain_text": "All plain text extracted from PDF file"}
.
- input: PDF file, by
-
/anonymize/txt
- input: txt plain text file, by
POST
, as form-data. Expected POST param name is:file
. See an example of file structure on this sample file. - output: JSON like
{"patient_id": "1234", "document_text": "She's in good health!"}
.
- input: txt plain text file, by
Install docker, and docker-compose.
Clone this repo and build Docker containers with docker-compose
git clone [email protected]:serfer2/flask-hexagonal-architecture-api.git
cd flask-hexagonal-architecture-api
docker-compose up -d
Now, API is listening at localhost port 8000. Available endpoints are:
Code architecture follows hexagonal architecture principles, also known as ports and adapters.
This architecture is divided in three main layers:
-
Infrastructure: The outer layer. Controllers and all I/O related stuff (web framework, DB, ...). Anything that can change by an "external" cause (not by your decision), is in this layer. It includes repositories specific implementation, known as adapters.
-
Application: Use cases. Actions triggered by API calls, represented by application services.
-
Domain: Inner layer. Business context and rules goes here, represented by models and domain services. Repositories Interfaces, known as ports, belongs to this layer.
Source code directory tree looks like this:
.
├── application
│ ├── exceptions.py
│ ├── __init__.py
│ └── services
│ ├── acquire_pdf_file.py
│ ├── anonymize_txt_file.py
│ └── __init__.py
├── controller
│ ├── app.py ---------------> API entry point
│ ├── exceptions.py
│ ├── __init__.py
│ └── utils.py
├── Dockerfile
├── domain
│ ├── __init__.py
│ ├── models
│ │ ├── __init__.py
│ │ └── report.py
│ └── repositories
│ ├── __init__.py
│ └── report_repository_interface.py
├── infrastructure
│ ├── database.py
│ ├── __init__.py
│ └── repositories
│ ├── base_repository.py
│ ├── __init__.py
│ └── report_repository.py
├── lint.sh
├── requirements.txt
└── test
├── application
│ ├── __init__.py
│ └── test_services.py
├── base_test_case.py
├── controller
│ ├── __init__.py
│ └── test_app.py
├── infrastructure
│ ├── __init__.py
│ └── test_repositories.py
└── __init__.py
Tests have been built with Unittest. Running tests is as simple as:
docker-compose exec api python -m unittest discover
-
We're testing full API call lifetime cycle, see
test/controller/test_app.py
. These tests are not acceptance tests but close to be. -
Application services have been tested trough
test/controller/test_app.py
, but some extra unit tests have been needed, these tests are intest/application/test__services.py
. -
Since API is very simple, we don't have domain services. Models are close to be DTOs, so there's no logic to be tested. This is why there's no
test/domain/
folder. -
According to the architecture schema (see image above), integration tests are in
test/infrastructure/test_repositories.py
.
Postman is a great tool for playing with our API.
Here's an example on how to send files by HTTP POST method to our API endpoints:
- [ES] Curso de arquitectura Hexagonal de codely.tv
- Flask development guide.
- SQLAlchemy documentation.
Since this is a demo project, extracting plain text from PDF, is out of scope. This is why AcquirePdfFile
application service is incomplete (actually returning a hardcoded string). Feel free to complete it :)