Skip to content

Commit 6f58de5

Browse files
committed
feat: Initial commit
0 parents  commit 6f58de5

File tree

104 files changed

+2149
-0
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

104 files changed

+2149
-0
lines changed

.editorconfig

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# EditorConfig is awesome: https://EditorConfig.org
2+
3+
# top-most EditorConfig file
4+
root = true
5+
6+
[*]
7+
indent_style = space
8+
indent_size = 2
9+
end_of_line = lf
10+
charset = utf-8
11+
trim_trailing_whitespace = true
12+
insert_final_newline = true
13+
14+
[*.py]
15+
indent_style = space
16+
indent_size = 4

.env.example

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
DATABASE_USER="db_user"
2+
DATABASE_PASSWORD="db_pass"
3+
DATABASE_NAME="db_name"
4+
DATABASE_HOST="localhost"
5+
DATABASE_PORT="5432"

.gitignore

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
**/__pycache__
2+
**/.pytest_cache
3+
venv
4+
.env
5+
6+
# app
7+
app.log

.pre-commit-config.yaml

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
repos:
2+
- repo: https://github.com/PyCQA/flake8
3+
rev: 7.0.0
4+
hooks:
5+
- id: flake8
6+
exclude: ^(venv|alembic)/
7+
8+
- repo: local
9+
hooks:
10+
- id: requirements
11+
name: requirements
12+
language: system
13+
entry: bash -c "pip3 freeze > requirements.txt; git add requirements.txt"
14+
pass_filenames: false
15+
stages: [commit]

.vscode/settings.json

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"files.exclude": {
3+
"**/__pycache__": true,
4+
"**/.pytest_cache": true,
5+
},
6+
"[python]": {
7+
"editor.rulers": [
8+
90
9+
]
10+
}
11+
}

LICENSE

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2024 Wesley Barbosa
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

+102
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
<div align="center">
2+
<a href="https://www.python.org/"><img src="https://upload.wikimedia.org/wikipedia/commons/c/c3/Python-logo-notext.svg" width="100"></a>
3+
</div>
4+
5+
<h1 align="center">Python Clean Architecture</h1>
6+
7+
Clean architecture, as defined by [Robert C. Martin](https://en.wikipedia.org/wiki/Robert_C._Martin) in his book **Clean Architecture: A Craftsman's Guide to Software Structure and Design** emphasizes the separation of concerns into distinct layers to promote modularity, testing, and code maintenance.
8+
9+
## Installation
10+
1. Clone the repository to your local environment:
11+
```bash
12+
git clone https://github.com/wesleey/python-clean-architecture.git
13+
```
14+
2. Navigate to the project directory:
15+
```bash
16+
cd python-clean-architecture
17+
```
18+
3. Create and activate a virtual environment:
19+
```bash
20+
python -m venv venv
21+
source venv/bin/activate # For Unix/macOS
22+
venv\Scripts\activate # For Windows
23+
```
24+
4. Install dependencies using pip3:
25+
```
26+
pip3 install -r requirements.txt
27+
```
28+
5. Install pre-commit hooks:
29+
```bash
30+
pre-commit install
31+
```
32+
33+
## Database Migrations
34+
After installing and configuring the database, you can apply the necessary migrations using the following command:
35+
```bash
36+
alembic upgrade head
37+
```
38+
39+
## Usage
40+
### Run the In-Memory CLI
41+
```bash
42+
python cli_memory_process_handler.py
43+
```
44+
### Run the In-Memory Flask API
45+
```bash
46+
python flask_memory_process_handler.py
47+
```
48+
### Run the PostgreSQL Flask API
49+
```bash
50+
python flask_process_handler.py
51+
```
52+
53+
## Testing Flask APIs
54+
```bash
55+
curl -X POST -H "Content-Type: application/json" -d '{"first_name": "John", "last_name": "Doe", "email": "[email protected]"}' http://localhost:5000/v1/user/
56+
```
57+
Make sure the Flask server is running before making requests using cURL.
58+
59+
## Commands
60+
### Docker
61+
#### Starts containers in the background
62+
```bash
63+
docker compose up -d
64+
```
65+
#### Stops containers
66+
```bash
67+
docker compose down
68+
```
69+
#### Show all running containers
70+
```bash
71+
docker ps
72+
```
73+
#### Open shell in running container
74+
```bash
75+
docker exec -it <container_id> bash
76+
```
77+
#### Exit the shell
78+
```bash
79+
exit
80+
```
81+
### PostgreSQL
82+
#### Show all users
83+
```bash
84+
psql -U db_user -d db_name -c "SELECT * FROM users"
85+
```
86+
#### Delete all users
87+
```bash
88+
psql -U db_user -d db_name -c "DELETE FROM users"
89+
```
90+
#### Delete user by email
91+
```bash
92+
psql -U db_user -d db_name -c "DELETE FROM users WHERE email = '<user_email>'"
93+
```
94+
95+
## References
96+
- [Python Clean Architecture In-memory CLI implementation](https://www.linkedin.com/pulse/implementation-clean-architecture-python-part-1-cli-watanabe/)
97+
- [Error Handling, Logging and Validation implementation in Python Clean Architecture](https://www.linkedin.com/pulse/implementation-clean-architecture-python-part-2-error-watanabe/)
98+
- [Python Clean Architecture Flask Web API In-memory implementation](https://www.linkedin.com/pulse/implementation-clean-architecture-python-part-3-adding-watanabe/)
99+
- [Python Clean Architecture Flask Web API Postgresql implementation](https://github.com/claudiosw/python-clean-architecture-example/pulse/implementation-clean-architecture-python-part-4-adding-watanabe)
100+
101+
## License
102+
This project is licensed under the [MIT License](./LICENSE).

alembic.ini

+115
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
# A generic, single database configuration.
2+
3+
[alembic]
4+
# path to migration scripts
5+
script_location = alembic
6+
7+
# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
8+
# Uncomment the line below if you want the files to be prepended with date and time
9+
# see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file
10+
# for all available tokens
11+
# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s
12+
13+
# sys.path path, will be prepended to sys.path if present.
14+
# defaults to the current working directory.
15+
prepend_sys_path = .
16+
17+
# timezone to use when rendering the date within the migration file
18+
# as well as the filename.
19+
# If specified, requires the python>=3.9 or backports.zoneinfo library.
20+
# Any required deps can installed by adding `alembic[tz]` to the pip requirements
21+
# string value is passed to ZoneInfo()
22+
# leave blank for localtime
23+
# timezone =
24+
25+
# max length of characters to apply to the
26+
# "slug" field
27+
# truncate_slug_length = 40
28+
29+
# set to 'true' to run the environment during
30+
# the 'revision' command, regardless of autogenerate
31+
# revision_environment = false
32+
33+
# set to 'true' to allow .pyc and .pyo files without
34+
# a source .py file to be detected as revisions in the
35+
# versions/ directory
36+
# sourceless = false
37+
38+
# version location specification; This defaults
39+
# to alembic/versions. When using multiple version
40+
# directories, initial revisions must be specified with --version-path.
41+
# The path separator used here should be the separator specified by "version_path_separator" below.
42+
# version_locations = %(here)s/bar:%(here)s/bat:alembic/versions
43+
44+
# version path separator; As mentioned above, this is the character used to split
45+
# version_locations. The default within new alembic.ini files is "os", which uses os.pathsep.
46+
# If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas.
47+
# Valid values for version_path_separator are:
48+
#
49+
# version_path_separator = :
50+
# version_path_separator = ;
51+
# version_path_separator = space
52+
version_path_separator = os # Use os.pathsep. Default configuration used for new projects.
53+
54+
# set to 'true' to search source files recursively
55+
# in each "version_locations" directory
56+
# new in Alembic version 1.10
57+
# recursive_version_locations = false
58+
59+
# the output encoding used when revision files
60+
# are written from script.py.mako
61+
# output_encoding = utf-8
62+
63+
# sqlalchemy.url = driver://user:pass@localhost/dbname
64+
65+
[post_write_hooks]
66+
# post_write_hooks defines scripts or Python functions that are run
67+
# on newly generated revision scripts. See the documentation for further
68+
# detail and examples
69+
70+
# format using "black" - use the console_scripts runner, against the "black" entrypoint
71+
# hooks = black
72+
# black.type = console_scripts
73+
# black.entrypoint = black
74+
# black.options = -l 79 REVISION_SCRIPT_FILENAME
75+
76+
# lint with attempts to fix using "ruff" - use the exec runner, execute a binary
77+
# hooks = ruff
78+
# ruff.type = exec
79+
# ruff.executable = %(here)s/.venv/bin/ruff
80+
# ruff.options = --fix REVISION_SCRIPT_FILENAME
81+
82+
# Logging configuration
83+
[loggers]
84+
keys = root,sqlalchemy,alembic
85+
86+
[handlers]
87+
keys = console
88+
89+
[formatters]
90+
keys = generic
91+
92+
[logger_root]
93+
level = WARN
94+
handlers = console
95+
qualname =
96+
97+
[logger_sqlalchemy]
98+
level = WARN
99+
handlers =
100+
qualname = sqlalchemy.engine
101+
102+
[logger_alembic]
103+
level = INFO
104+
handlers =
105+
qualname = alembic
106+
107+
[handler_console]
108+
class = StreamHandler
109+
args = (sys.stderr,)
110+
level = NOTSET
111+
formatter = generic
112+
113+
[formatter_generic]
114+
format = %(levelname)-5.5s [%(name)s] %(message)s
115+
datefmt = %H:%M:%S

alembic/README

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Generic single-database configuration.

alembic/env.py

+84
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
from logging.config import fileConfig
2+
3+
from sqlalchemy import engine_from_config
4+
from sqlalchemy import pool
5+
6+
from alembic import context
7+
8+
from configs import config as config_app
9+
from src.infra.db.settings.base import Base
10+
from src.infra.db.models.users_db_model import UsersDBModel
11+
12+
# this is the Alembic Config object, which provides
13+
# access to the values within the .ini file in use.
14+
config = context.config
15+
16+
17+
config.set_main_option('sqlalchemy.url', config_app.DB_URI)
18+
19+
20+
# Interpret the config file for Python logging.
21+
# This line sets up loggers basically.
22+
if config.config_file_name is not None:
23+
fileConfig(config.config_file_name)
24+
25+
# add your model's MetaData object here
26+
# for 'autogenerate' support
27+
# from myapp import mymodel
28+
# target_metadata = mymodel.Base.metadata
29+
target_metadata = Base.metadata
30+
31+
# other values from the config, defined by the needs of env.py,
32+
# can be acquired:
33+
# my_important_option = config.get_main_option("my_important_option")
34+
# ... etc.
35+
36+
37+
def run_migrations_offline() -> None:
38+
"""Run migrations in 'offline' mode.
39+
40+
This configures the context with just a URL
41+
and not an Engine, though an Engine is acceptable
42+
here as well. By skipping the Engine creation
43+
we don't even need a DBAPI to be available.
44+
45+
Calls to context.execute() here emit the given string to the
46+
script output.
47+
48+
"""
49+
url = config.get_main_option("sqlalchemy.url")
50+
context.configure(
51+
url=url,
52+
target_metadata=target_metadata,
53+
literal_binds=True,
54+
dialect_opts={"paramstyle": "named"},
55+
)
56+
57+
with context.begin_transaction():
58+
context.run_migrations()
59+
60+
61+
def run_migrations_online() -> None:
62+
"""Run migrations in 'online' mode.
63+
64+
In this scenario we need to create an Engine
65+
and associate a connection with the context.
66+
67+
"""
68+
connectable = engine_from_config(
69+
config.get_section(config.config_ini_section, {}),
70+
prefix="sqlalchemy.",
71+
poolclass=pool.NullPool,
72+
)
73+
74+
with connectable.connect() as connection:
75+
context.configure(connection=connection, target_metadata=target_metadata)
76+
77+
with context.begin_transaction():
78+
context.run_migrations()
79+
80+
81+
if context.is_offline_mode():
82+
run_migrations_offline()
83+
else:
84+
run_migrations_online()

0 commit comments

Comments
 (0)