├── .gitignore ├── LICENSE ├── README.md ├── VERSION ├── cookiecutter.json └── {{cookiecutter.project_name}} ├── alembic.ini ├── alembic ├── README ├── env.py ├── script.py.mako └── versions │ └── c79001bbe89d_make_models.py ├── apps ├── __init__.py ├── health_check │ ├── constants │ │ └── __init__.py │ ├── depends │ │ └── __init__.py │ ├── models │ │ └── __init__.py │ ├── serializers │ │ └── __init__.py │ ├── tests │ │ ├── __init__.py │ │ └── test_health_check.py │ ├── urls.py │ └── views │ │ ├── __init__.py │ │ └── health_check.py ├── hello_world │ ├── __init__.py │ ├── models │ │ └── __init__.py │ ├── serializers │ │ └── __init__.py │ ├── test │ │ └── test_hello_world.py │ ├── urls.py │ └── views │ │ ├── __init__.py │ │ └── hello_world.py ├── oauth2 │ ├── constants │ │ └── __init__.py │ ├── depends │ │ ├── __init__.py │ │ └── app.py │ ├── models │ │ ├── __init__.py │ │ ├── access_token.py │ │ └── apps.py │ ├── serializers │ │ ├── __init__.py │ │ ├── app.py │ │ └── token_access.py │ ├── services │ │ └── __init__.py │ ├── test │ │ ├── __init__.py │ │ ├── test_get_access_protected_app.py │ │ └── test_get_access_token.py │ ├── urls.py │ └── views │ │ ├── __init__.py │ │ ├── token_access.py │ │ └── view_protected.py └── token │ ├── __init__.py │ ├── constants │ ├── __init__.py │ └── jwt.py │ ├── depends │ ├── __init__.py │ ├── get_jwt.py │ └── get_token_decode.py │ ├── models │ └── __init__.py │ ├── serializers │ └── __init__.py │ ├── test │ ├── __init__.py │ └── test_verify_token.py │ ├── urls.py │ └── views │ ├── __init__.py │ └── token.py ├── conftest.py ├── core ├── __init__.py ├── config.py ├── db │ ├── base.py │ └── setup.py ├── depends │ ├── __init__.py │ ├── get_config.py │ ├── get_database.py │ ├── get_host_remote.py │ └── get_object.py ├── middlewares │ ├── __init__.py │ ├── database.py │ └── settings.py ├── serializers │ ├── __init__.py │ └── message.py ├── test │ ├── __init__.py │ └── transaction_test_case.py ├── urls.py └── utils │ ├── get_object_or_404.py │ ├── init_config.py │ └── init_db.py ├── deployments ├── docker-compose.yml └── run.sh ├── main.py ├── requirements.txt └── scripts ├── generate_dummy_data.sh ├── makemigrations.sh ├── migrate.sh ├── runserver-dev.sh ├── runserver.sh └── startapp.sh /.gitignore: -------------------------------------------------------------------------------- 1 | # cache 2 | 3 | __pycache__ 4 | 5 | # pycharm 6 | .idea/* 7 | 8 | # virtual environment 9 | 10 | venv -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Ángel Berhó Grande 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FastApi base project 2 | 3 | This is a base project to start project very easy fast api app from FastApi with python >3.6 version. 4 | 5 | 6 | ### How to install? 7 | 8 | To initialize project is necessary to install cookie-cutter. 9 | 10 | * `pip install cookiecutter` # install cookiecutter 11 | * `cookiecutter https://github.com/bergran/fast-api-project-template` # download project template. It will prompt project name. 12 | * `pip install -r requirements.txt` # install libraries to getting started 13 | 14 | To check template works, it have 2 endpoints and 5 tests 15 | 16 | * hello_world endpoint. It returns `{'message': 'hello world'}` with status code `200`. 17 | * verify token endpoint. It returns `{'token': ''}` with status code `200`. 18 | 19 | 20 | execute `pytest`. 21 | 22 | 23 | ### How to deploy it? 24 | 25 | - docker-compose -f deployments/docker-compose.yml -p up -d 26 | 27 | This command runs db and app into 2 containers and it does to has another context. This is 28 | very interesant because if you are builting microservices it will run as separated environments 29 | 30 | 31 | ### How to run project? 32 | 33 | It's easy, you two options to run project. 34 | 35 | - sh scripts/runserver.sh 36 | - python main.py 37 | 38 | 39 | ### How to make migrations? 40 | 41 | After create your models, you can use alembic directly or use script. 42 | 43 | - `sh scripts/makemigrations "Some info about your migration"` 44 | - `alembic revision --autogenerate -m "Some info about your migration"` 45 | 46 | and migrate it on database: 47 | 48 | - `sh scripts/migrate` 49 | - `PYTHONPATH=.; alembic upgrade` 50 | 51 | 52 | ### Features 53 | 54 | * User model agnostic. you choose your models 55 | * Database and migrations with SqlAlchemy and Alembic. 56 | * JWT Integration extraction only. 57 | * TransactionTestCase, that means it could querying with test. 58 | * 59 | 60 | ### Architecture 61 | 62 | 63 | 60/5000 64 | The architecture is made for medium-sized backends and It is placed in the following directory structure: 65 | 66 | * alembic: database Migrations, you should change nothing here unless you need to remove 67 | bad migration. Then just go to versions and remove them. 68 | * apps: It is our api code, separated in some application modules. Later we will comment more about it. 69 | * core: is where our code is shared among apps, for example: middlewares, dependencies, serializers, utils, etc. 70 | * deployments: Here i put a docker-compose file, i do not suggest to use on production environment, only to develop and pre-prod 71 | * scripts: Here i put some bash scripts to start fast project: 72 | * makemigrations: Create migrations/revisions from our models. 73 | * params: description about migration. 74 | * migrate: Apply migrations/revisions on Database. 75 | * runserver: Start server on 0.0.0.0 with hot reload. 76 | * startapp: create a skeleton app on apps directory. 77 | * params: app name. 78 | 79 | ### 1. App modules 80 | 81 | Apps are part from our api/ws/graphQl logic. It's separated on: 82 | * models: Database models. 83 | * serializers: Here goes pydantic or serializers third party to your data. 84 | * test: As his name said, here it is saved our test. 85 | * views: Here goes our views logic. I'm not sure if i should separated api from ws and GraphQl views. 86 | At this time, all is here. 87 | * depends: Here we save our magic. If you do not know about what is Depends click [here](https://fastapi.tiangolo.com/tutorial/dependencies/first-steps/). 88 | * urls: Here we group all our router views to export it as app. 89 | 90 | **Important**: To include app on project, you has to include app name into `apps` config and 91 | include routes on core/urls.py as `app.include_router` 92 | 93 | ### 2. Database 94 | 95 | Project database is configured to work with postgres, but it can allow work with 96 | databases that are compatible with SqlAlchemy (MySql, Oracle, Sqlite and more...) for more 97 | information you can click [here](https://www.sqlalchemy.org/features.html). 98 | 99 | #### 2.1 Starting with models 100 | 101 | As i said before, it's configure to work with SqlAlchemy. I create a shared `Base` to 102 | inherit into the models on `core.db.meta.Meta` with `__tablename__` with model name with lower format. 103 | 104 | **Important**: remember that models it's a module, so you have to include all your models on `__init__` into 105 | the `__all__` variable to be recognized. 106 | 107 | model example: 108 | 109 | ``` 110 | # -*- coding: utf-8 -*- 111 | import datetime 112 | 113 | from sqlalchemy import Column, Integer, String, DateTime 114 | 115 | from core.db.base import Base 116 | 117 | 118 | class Item(Base): 119 | __tablename__ = 'Items' 120 | id = Column(Integer, primary_key=True) 121 | name = Column(String) 122 | created = Column(DateTime, default=datetime.datetime.utcnow) 123 | modified = Column(DateTime, default=datetime.datetime.utcnow) 124 | 125 | ``` 126 | 127 | #### 2.2 Querying 128 | 129 | To query on your views, i added on `request.stage.session` the session as **transaction**, that means 130 | if any exception get raised this gonna `rollback` all data info as block. 131 | 132 | To know more about query api you can click [here](https://docs.sqlalchemy.org/en/13/orm/tutorial.html#querying) 133 | 134 | 135 | #### 3. JWT integration 136 | 137 | Integration with JWT is done with pyjwt, at this time just exist a `Depends` to get 138 | jwt and other to get payload decoded. 139 | 140 | #### 3.1 Depends 141 | 142 | * get_jwt: it will regex always `Authorization` header with the header config that you 143 | set it or default `JWT`. If header does not exist or has not `^{header} [A-Za-z0-9-_=]+\.[A-Za-z0-9-_=]+\.?[A-Za-z0-9-_.+/=]*$` 144 | format then will raise `HTTPException` and response with status code `400`. 145 | 146 | * get_token_decoded: it will check if jwt is valid and return payload. If token is expirate 147 | it will raise `HTTPException` and response with status code `400`. 148 | 149 | 150 | #### 4. Core 151 | 152 | This is the shared code for our app, here we can find some utils to declare a model, some depends, serializers, 153 | transactional test case, utils and config 154 | 155 | #### 4.1 depends 156 | 157 | It just like wizards, they can not make magic without his staff, we either can not make magic without depends on our views. 158 | 159 | I declare some depends to reuse all time that we need it. 160 | 161 | * get_config: this depend will return app config from request. 162 | * get_database: it will return session database (Transaction) 163 | * get_object: it will get object or will return response with status `404` 164 | 165 | #### 4.2 config 166 | 167 | I'm sorry, i tried to do all light that i can posible ¯\_( ͡° ͜ʖ ͡°)_/¯. 168 | 169 | 170 | ``` 171 | BASE_DIR: absolute path to directory project 172 | TEST_RUN: set this variable on make it test to get dummy database 173 | SECRET_KEY: app secret 174 | APPS: App list installed to create migrations 175 | JWT_EXPIRATION_DELTA: Time to expirate jwt token 176 | JWT_REFRESH_EXPIRATION_DELTA: Time to refresh token without fail 177 | JWT_AUTH_HEADER_PREFIX: work that will start Authorization header, default: JWT 178 | JWT_SECRET_KEY: secret to sign jwt 179 | BACKEND_CORS_ORIGINS: url list to allow requests 180 | PROJECT_NAME: Project name 181 | SENTRY_DSN: 182 | SMTP_TLS: - 183 | SMTP_PORT: - 184 | SMTP_HOST: - 185 | SMTP_USER: - 186 | SMTP_PASSWORD: - 187 | EMAILS_FROM_EMAIL: - 188 | EMAILS_FROM_NAME: - 189 | EMAIL_RESET_TOKEN_EXPIRE_HOURS: - 190 | EMAIL_TEMPLATES_DIR: - 191 | EMAILS_ENABLED: - 192 | DATABASES: it's a dictionary with database information 193 | ``` 194 | 195 | You can set all of these variables with environment variable. 196 | 197 | 198 | ### Testing 199 | 200 | It's implemented with pytest so it's easy to make test for each module app with the autodiscover feature. 201 | Also i created `TransactionalTestCase` to create on `setUp` some information for your api and check it later making requests with client. 202 | 203 | On finish each test it will delete all data store and it will create again with `setUp` to have always clean environment. 204 | If you have to overwrite `tearDown` class, you should use super method to clean environment after test pass. 205 | 206 | On finish all tests, it will delete test database. 207 | 208 | 209 | Fixtures: 210 | 211 | * session: database session, you can get it on `self.session` 212 | * client: client to make request, you can get it on `self.client` 213 | * base: base del, you can get it on `self.Base` 214 | 215 | 216 | ### Generate dummy data 217 | 218 | In this template it's installed factory-boy library so if you want to do it, go [here](https://factoryboy.readthedocs.io/en/latest/orms.html#managing-sessions) 219 | 220 | 221 | ### Oauth2 222 | 223 | It's implemented oauth2 (not all methods) token authorization and check if the app has enough scope to get or set data, 224 | at this moment is in beta version. 225 | 226 | To start, this should add some migrations to your service to add on your database, these models are: 227 | 228 | * App 229 | * Access Token 230 | 231 | #### App 232 | 233 | It's the instance which you are gonna log in and access to api. 234 | 235 | Properties are: 236 | * name: app name. String 237 | * created: when it was created. Datetime 238 | * modified: when it was modified (not implemented signal). Datetime 239 | * client_id: it's the id to get access token. String uuid. 240 | * client_secret: it's the secret to get access token. String uuid. 241 | 242 | #### AccessToken 243 | 244 | It's the instance which will storage access token and scopes to get access api. 245 | 246 | Properties are: 247 | * created: when it was created. Datetime 248 | * modified: when it was modified (not implemented signal). Datetime 249 | * app: it's the foreign key to make relation with app. ForeignKey. 250 | * scopes: are to see what scopes are allowed to the app. Array[string] 251 | * access_token: it is the token which the app will authorize. 252 | 253 | #### How to play? 254 | 255 | 1. We create a new app. it does not need to set client_id or client_secret 256 | it will set uuid4 by default (recomended unless need it do manual). 257 | 2. make a post request to `/token` with the next payload: 258 | ``` 259 | { 260 | "client_id": , 261 | "client_secret": , 262 | "scopes": [], # these scope it's gonna be setting on app settings config by default. [optional] 263 | "grant_type": "client_credentials", # method to login app 264 | } 265 | ``` 266 | 267 | headers 268 | 269 | ``` 270 | { 271 | "Content-Type": "application/x-www-form-urlencoded" 272 | } 273 | ``` 274 | 275 | response: status code `200` 276 | 277 | ``` 278 | { 279 | "access_token": access_token.access_token, # String uuid 280 | "token_type": "bearer" 281 | } 282 | ``` 283 | 3. With the access token, now we can access to `/me`. Method get. 284 | 285 | headers 286 | ``` 287 | { 288 | "authorization": "bearer " 289 | } 290 | ``` 291 | 292 | response status code `200` 293 | 294 | ``` 295 | { 296 | "name": , 297 | "created": , 298 | "modified": 299 | } 300 | ``` 301 | 302 | #### depends 303 | 304 | There are 2 depends to get the app token 305 | 306 | 1. get_app_object_access: Get app from client_id and client_secret. 307 | 2. get_app_object_scopes: Get app from authorization header and check it scopes given to the app. 308 | 309 | 310 | ### Framework origin 311 | 312 | [FastApi](https://fastapi.tiangolo.com/) docs 313 | 314 | [Sebastián Ramírez](https://github.com/tiangolo) creator 315 | 316 | 317 | ### Author project base 318 | 319 | [Angel Berhó Grande](https://github.com/bergran) 320 | 321 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 0.0.1 -------------------------------------------------------------------------------- /cookiecutter.json: -------------------------------------------------------------------------------- 1 | { 2 | "project_name": "FastApi template project" 3 | } 4 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/alembic.ini: -------------------------------------------------------------------------------- 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 files 8 | # file_template = %%(rev)s_%%(slug)s 9 | 10 | # timezone to use when rendering the date 11 | # within the migration file as well as the filename. 12 | # string value is passed to dateutil.tz.gettz() 13 | # leave blank for localtime 14 | # timezone = 15 | 16 | # max length of characters to apply to the 17 | # "slug" field 18 | # truncate_slug_length = 40 19 | 20 | # set to 'true' to run the environment during 21 | # the 'revision' command, regardless of autogenerate 22 | # revision_environment = false 23 | 24 | # set to 'true' to allow .pyc and .pyo files without 25 | # a source .py file to be detected as revisions in the 26 | # versions/ directory 27 | # sourceless = false 28 | 29 | # version location specification; this defaults 30 | # to alembic/versions. When using multiple version 31 | # directories, initial revisions must be specified with --version-path 32 | # version_locations = %(here)s/bar %(here)s/bat alembic/versions 33 | 34 | # the output encoding used when revision files 35 | # are written from script.py.mako 36 | # output_encoding = utf-8 37 | 38 | # sqlalchemy.url = postgresql://fastapi:fastapi@localhost:5432/fastapi 39 | 40 | 41 | # Logging configuration 42 | [loggers] 43 | keys = root,sqlalchemy,alembic 44 | 45 | [handlers] 46 | keys = console 47 | 48 | [formatters] 49 | keys = generic 50 | 51 | [logger_root] 52 | level = WARN 53 | handlers = console 54 | qualname = 55 | 56 | [logger_sqlalchemy] 57 | level = WARN 58 | handlers = 59 | qualname = sqlalchemy.engine 60 | 61 | [logger_alembic] 62 | level = INFO 63 | handlers = 64 | qualname = alembic 65 | 66 | [handler_console] 67 | class = StreamHandler 68 | args = (sys.stderr,) 69 | level = NOTSET 70 | formatter = generic 71 | 72 | [formatter_generic] 73 | format = %(levelname)-5.5s [%(name)s] %(message)s 74 | datefmt = %H:%M:%S 75 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/alembic/README: -------------------------------------------------------------------------------- 1 | Generic single-database configuration. -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/alembic/env.py: -------------------------------------------------------------------------------- 1 | import importlib 2 | from logging.config import fileConfig 3 | 4 | from alembic import context 5 | from sqlalchemy import engine_from_config 6 | from sqlalchemy import pool 7 | 8 | # this is the Alembic Config object, which provides 9 | # access to the values within the .ini file in use. 10 | from core.utils.init_config import init_config 11 | from core.utils.init_db import get_dsn 12 | 13 | config = context.config 14 | config_app = init_config() 15 | dsn = get_dsn(config_app) 16 | 17 | # Interpret the config file for Python logging. 18 | # This line sets up loggers basically. 19 | fileConfig(config.config_file_name) 20 | # set endpoint from config 21 | config.set_main_option('sqlalchemy.url', dsn) 22 | 23 | # add your model's MetaData object here 24 | # for 'autogenerate' support 25 | # from myapp import mymodel 26 | # target_metadata = mymodel.Base.metadata 27 | 28 | target_metadata = [] 29 | for app in config_app.APPS: 30 | i = importlib.import_module('apps.{}.models'.format(app)) 31 | 32 | if len(target_metadata) > 0: 33 | continue 34 | elif hasattr(i, '__all__') and len(i.__all__) > 0: 35 | model = i.__all__[0] 36 | target_metadata.append(getattr(i, model).metadata) 37 | 38 | 39 | # target_metadata = [Base.metadata] 40 | 41 | 42 | # other values from the config, defined by the needs of env.py, 43 | # can be acquired: 44 | # my_important_option = config.get_main_option("my_important_option") 45 | # ... etc. 46 | 47 | 48 | def run_migrations_offline(): 49 | """Run migrations in 'offline' mode. 50 | 51 | This configures the context with just a URL 52 | and not an Engine, though an Engine is acceptable 53 | here as well. By skipping the Engine creation 54 | we don't even need a DBAPI to be available. 55 | 56 | Calls to context.execute() here emit the given string to the 57 | script output. 58 | 59 | """ 60 | url = config.get_main_option("sqlalchemy.url") 61 | context.configure( 62 | url=url, target_metadata=target_metadata, literal_binds=True 63 | ) 64 | 65 | with context.begin_transaction(): 66 | context.run_migrations() 67 | 68 | 69 | def run_migrations_online(): 70 | """Run migrations in 'online' mode. 71 | 72 | In this scenario we need to create an Engine 73 | and associate a connection with the context. 74 | 75 | """ 76 | connectable = engine_from_config( 77 | config.get_section(config.config_ini_section), 78 | prefix="sqlalchemy.", 79 | poolclass=pool.NullPool, 80 | ) 81 | 82 | with connectable.connect() as connection: 83 | context.configure( 84 | connection=connection, target_metadata=target_metadata 85 | ) 86 | 87 | with context.begin_transaction(): 88 | context.run_migrations() 89 | 90 | 91 | if context.is_offline_mode(): 92 | run_migrations_offline() 93 | else: 94 | run_migrations_online() 95 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/alembic/script.py.mako: -------------------------------------------------------------------------------- 1 | """${message} 2 | 3 | Revision ID: ${up_revision} 4 | Revises: ${down_revision | comma,n} 5 | Create Date: ${create_date} 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | ${imports if imports else ""} 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = ${repr(up_revision)} 14 | down_revision = ${repr(down_revision)} 15 | branch_labels = ${repr(branch_labels)} 16 | depends_on = ${repr(depends_on)} 17 | 18 | 19 | def upgrade(): 20 | ${upgrades if upgrades else "pass"} 21 | 22 | 23 | def downgrade(): 24 | ${downgrades if downgrades else "pass"} 25 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/alembic/versions/c79001bbe89d_make_models.py: -------------------------------------------------------------------------------- 1 | """make models 2 | 3 | Revision ID: c79001bbe89d 4 | Revises: 5 | Create Date: 2019-10-14 01:19:56.971084 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | from sqlalchemy.dialects import postgresql 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = 'c79001bbe89d' 14 | down_revision = None 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.create_table('app', 22 | sa.Column('id', sa.Integer(), nullable=False), 23 | sa.Column('name', sa.String(length=254), nullable=True), 24 | sa.Column('created', sa.DateTime(), nullable=True), 25 | sa.Column('modified', sa.DateTime(), nullable=True), 26 | sa.Column('client_id', sa.String(length=36), nullable=True), 27 | sa.Column('client_secret', sa.String(length=36), nullable=True), 28 | sa.PrimaryKeyConstraint('id') 29 | ) 30 | op.create_table('accesstoken', 31 | sa.Column('id', sa.Integer(), nullable=False), 32 | sa.Column('created', sa.DateTime(), nullable=True), 33 | sa.Column('modified', sa.DateTime(), nullable=True), 34 | sa.Column('app', sa.Integer(), nullable=True), 35 | sa.Column('scopes', postgresql.ARRAY(sa.String()), nullable=True), 36 | sa.Column('access_token', sa.String(length=36), nullable=True), 37 | sa.ForeignKeyConstraint(['app'], ['app.id'], ), 38 | sa.PrimaryKeyConstraint('id') 39 | ) 40 | # ### end Alembic commands ### 41 | 42 | 43 | def downgrade(): 44 | # ### commands auto generated by Alembic - please adjust! ### 45 | op.drop_table('accesstoken') 46 | op.drop_table('app') 47 | # ### end Alembic commands ### 48 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/apps/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bergran/fast-api-project-template/ad29b3f6d37bb3653efff66cf0d8c76ce6015bb5/{{cookiecutter.project_name}}/apps/__init__.py -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/apps/health_check/constants/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bergran/fast-api-project-template/ad29b3f6d37bb3653efff66cf0d8c76ce6015bb5/{{cookiecutter.project_name}}/apps/health_check/constants/__init__.py -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/apps/health_check/depends/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bergran/fast-api-project-template/ad29b3f6d37bb3653efff66cf0d8c76ce6015bb5/{{cookiecutter.project_name}}/apps/health_check/depends/__init__.py -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/apps/health_check/models/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bergran/fast-api-project-template/ad29b3f6d37bb3653efff66cf0d8c76ce6015bb5/{{cookiecutter.project_name}}/apps/health_check/models/__init__.py -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/apps/health_check/serializers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bergran/fast-api-project-template/ad29b3f6d37bb3653efff66cf0d8c76ce6015bb5/{{cookiecutter.project_name}}/apps/health_check/serializers/__init__.py -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/apps/health_check/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bergran/fast-api-project-template/ad29b3f6d37bb3653efff66cf0d8c76ce6015bb5/{{cookiecutter.project_name}}/apps/health_check/tests/__init__.py -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/apps/health_check/tests/test_health_check.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from starlette import status 3 | 4 | from core.test.transaction_test_case import TransactionTestCase 5 | 6 | 7 | class HealthCheckTestCase(TransactionTestCase): 8 | 9 | def test_health_check(self): 10 | response = self.client.get('/api/v1/health-check') 11 | self.assertEqual(status.HTTP_200_OK, response.status_code) 12 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/apps/health_check/urls.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from fastapi.routing import APIRouter 4 | from apps.health_check.views.health_check import router as router_health_check 5 | 6 | 7 | router = APIRouter() 8 | router.include_router(router_health_check, prefix='/health-check') 9 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/apps/health_check/views/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bergran/fast-api-project-template/ad29b3f6d37bb3653efff66cf0d8c76ce6015bb5/{{cookiecutter.project_name}}/apps/health_check/views/__init__.py -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/apps/health_check/views/health_check.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from fastapi.routing import APIRouter 4 | from starlette import status 5 | from starlette.responses import Response 6 | 7 | router = APIRouter() 8 | 9 | 10 | @router.get('/') 11 | def health_check(): 12 | return Response(status_code=status.HTTP_200_OK) 13 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/apps/hello_world/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bergran/fast-api-project-template/ad29b3f6d37bb3653efff66cf0d8c76ce6015bb5/{{cookiecutter.project_name}}/apps/hello_world/__init__.py -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/apps/hello_world/models/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bergran/fast-api-project-template/ad29b3f6d37bb3653efff66cf0d8c76ce6015bb5/{{cookiecutter.project_name}}/apps/hello_world/models/__init__.py -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/apps/hello_world/serializers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bergran/fast-api-project-template/ad29b3f6d37bb3653efff66cf0d8c76ce6015bb5/{{cookiecutter.project_name}}/apps/hello_world/serializers/__init__.py -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/apps/hello_world/test/test_hello_world.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from starlette import status 3 | 4 | from core.test.transaction_test_case import TransactionTestCase 5 | 6 | 7 | class HelloWorldTestCase(TransactionTestCase): 8 | 9 | def test_get_response(self): 10 | response = self.client.get('/api/v1/') 11 | self.assertEqual(response.status_code, status.HTTP_200_OK) 12 | self.assertEqual(response.json(), {"message": "Hello World"}) -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/apps/hello_world/urls.py: -------------------------------------------------------------------------------- 1 | from fastapi.routing import APIRouter 2 | 3 | from apps.hello_world.views.hello_world import router as routes 4 | 5 | router = APIRouter() 6 | 7 | router.include_router(routes) 8 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/apps/hello_world/views/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bergran/fast-api-project-template/ad29b3f6d37bb3653efff66cf0d8c76ce6015bb5/{{cookiecutter.project_name}}/apps/hello_world/views/__init__.py -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/apps/hello_world/views/hello_world.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | from fastapi.routing import APIRouter 5 | 6 | router = APIRouter() 7 | 8 | 9 | @router.get("/") 10 | async def hello_world(): 11 | return {"message": "Hello World"} 12 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/apps/oauth2/constants/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bergran/fast-api-project-template/ad29b3f6d37bb3653efff66cf0d8c76ce6015bb5/{{cookiecutter.project_name}}/apps/oauth2/constants/__init__.py -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/apps/oauth2/depends/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bergran/fast-api-project-template/ad29b3f6d37bb3653efff66cf0d8c76ce6015bb5/{{cookiecutter.project_name}}/apps/oauth2/depends/__init__.py -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/apps/oauth2/depends/app.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from fastapi import Depends, Header 3 | from sqlalchemy import cast, String 4 | from sqlalchemy.dialects.postgresql import ARRAY 5 | from sqlalchemy.orm import Session, aliased 6 | 7 | from apps.oauth2.models import App, AccessToken 8 | from apps.oauth2.serializers.token_access import TokenAccess 9 | from core.depends import get_database 10 | from core.utils.get_object_or_404 import get_object_or_404 11 | 12 | 13 | def get_app_object_access(session: Session = Depends(get_database), access: TokenAccess = Depends()): 14 | return get_object_or_404(session.query(App).filter( 15 | App.client_secret == access.client_secret, 16 | App.client_id == access.client_id 17 | )) 18 | 19 | 20 | def get_app_object_scopes(scopes): 21 | def wrapper(session: Session = Depends(get_database), authorization: str = Header(..., alias='Authorization')): 22 | _, authorization_splited = authorization.split(' ') 23 | return get_object_or_404(session.query(App).join(AccessToken).filter( 24 | AccessToken.access_token == authorization_splited, 25 | AccessToken.scopes.contains(cast(scopes, ARRAY(String))) 26 | )) 27 | 28 | return wrapper 29 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/apps/oauth2/models/__init__.py: -------------------------------------------------------------------------------- 1 | from .apps import App 2 | from .access_token import AccessToken 3 | 4 | __all__ = ['App', 'AccessToken'] 5 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/apps/oauth2/models/access_token.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import datetime 3 | from uuid import uuid4 4 | 5 | from sqlalchemy import Column, Integer, String, DateTime, ForeignKey 6 | from sqlalchemy.dialects.postgresql import ARRAY 7 | 8 | 9 | from core.db.base import Base 10 | 11 | 12 | class AccessToken(Base): 13 | id = Column(Integer, primary_key=True) 14 | created = Column(DateTime, default=datetime.datetime.utcnow) 15 | modified = Column(DateTime, default=datetime.datetime.utcnow) 16 | app = Column(Integer, ForeignKey('app.id')) 17 | scopes = Column(ARRAY(String), default=[]) 18 | access_token = Column(String(36), default=uuid4) 19 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/apps/oauth2/models/apps.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import datetime 3 | from uuid import uuid4 4 | 5 | from sqlalchemy import Column, Integer, String, DateTime 6 | from sqlalchemy.orm import relationship 7 | 8 | from core.db.base import Base 9 | 10 | 11 | class App(Base): 12 | id = Column(Integer, primary_key=True) 13 | name = Column(String(254)) 14 | created = Column(DateTime, default=datetime.datetime.utcnow) 15 | modified = Column(DateTime, default=datetime.datetime.utcnow) 16 | client_id = Column(String(36), default=uuid4) 17 | client_secret = Column(String(36), default=uuid4) 18 | 19 | access_tokens = relationship('AccessToken') 20 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/apps/oauth2/serializers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bergran/fast-api-project-template/ad29b3f6d37bb3653efff66cf0d8c76ce6015bb5/{{cookiecutter.project_name}}/apps/oauth2/serializers/__init__.py -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/apps/oauth2/serializers/app.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from datetime import datetime 3 | 4 | from pydantic import BaseModel 5 | 6 | 7 | class AppOut(BaseModel): 8 | name: str 9 | created: datetime 10 | modified: datetime 11 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/apps/oauth2/serializers/token_access.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from fastapi import Form 4 | from pydantic import BaseModel 5 | 6 | 7 | class TokenAccess: 8 | 9 | def __init__( 10 | self, 11 | grant_type: str = Form(..., regex="^client_credentials$"), 12 | scope: str = Form(''), 13 | client_id: str = Form(None), 14 | client_secret: str = Form(None), 15 | ): 16 | self.grant_type = grant_type 17 | self.scopes = scope.split() 18 | self.client_id = client_id 19 | self.client_secret = client_secret 20 | 21 | 22 | class TokenOut(BaseModel): 23 | access_token: str 24 | token_type: str 25 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/apps/oauth2/services/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bergran/fast-api-project-template/ad29b3f6d37bb3653efff66cf0d8c76ce6015bb5/{{cookiecutter.project_name}}/apps/oauth2/services/__init__.py -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/apps/oauth2/test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bergran/fast-api-project-template/ad29b3f6d37bb3653efff66cf0d8c76ce6015bb5/{{cookiecutter.project_name}}/apps/oauth2/test/__init__.py -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/apps/oauth2/test/test_get_access_protected_app.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from starlette import status 4 | 5 | from apps.oauth2.models import App, AccessToken 6 | from core.test.transaction_test_case import TransactionTestCase 7 | from core.utils.get_object_or_404 import get_object 8 | 9 | 10 | class AccessTokenTestCase(TransactionTestCase): 11 | 12 | def setUp(self): 13 | session = self.session 14 | 15 | self.app_model = App(name='App test') 16 | 17 | session.add(self.app_model) 18 | session.commit() 19 | 20 | self.access_token_good = AccessToken(scopes=['me']) 21 | self.access_token_bad = AccessToken(scopes=['em']) 22 | self.access_token_superset = AccessToken(scopes=['me', 'em']) 23 | self.access_token_none = AccessToken(scopes=[]) 24 | 25 | self.app_model.access_tokens.append(self.access_token_bad) 26 | self.app_model.access_tokens.append(self.access_token_good) 27 | self.app_model.access_tokens.append(self.access_token_superset) 28 | self.app_model.access_tokens.append(self.access_token_none) 29 | session.commit() 30 | 31 | def test_get_app_good_scopes(self): 32 | headers = { 33 | 'Authorization': f'bearer {self.access_token_good.access_token}' 34 | } 35 | 36 | response = self.client.get('api/v1/me', headers=headers) 37 | 38 | self.assertEqual(response.status_code, status.HTTP_200_OK) 39 | 40 | payload = response.json() 41 | 42 | self.assertIn('name', payload) 43 | self.assertIn('created', payload) 44 | self.assertIn('modified', payload) 45 | 46 | def test_get_app_good_scopes_superset(self): 47 | headers = { 48 | 'Authorization': f'bearer {self.access_token_superset.access_token}' 49 | } 50 | 51 | response = self.client.get('api/v1/me', headers=headers) 52 | 53 | self.assertEqual(response.status_code, status.HTTP_200_OK) 54 | 55 | payload = response.json() 56 | 57 | self.assertIn('name', payload) 58 | self.assertIn('created', payload) 59 | self.assertIn('modified', payload) 60 | 61 | def test_get_app_good_scopes_none(self): 62 | headers = { 63 | 'Authorization': f'bearer {self.access_token_none.access_token}' 64 | } 65 | 66 | response = self.client.get('api/v1/me', headers=headers) 67 | 68 | self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) 69 | 70 | def test_get_app_bad_scopes(self): 71 | headers = { 72 | 'Authorization': f'bearer {self.access_token_bad.access_token}' 73 | } 74 | 75 | response = self.client.get('api/v1/me', headers=headers) 76 | 77 | self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) 78 | 79 | def test_get_app_bad_auth(self): 80 | headers = { 81 | 'Authorization': f'bearer fff' 82 | } 83 | 84 | response = self.client.get('api/v1/me', headers=headers) 85 | 86 | self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) 87 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/apps/oauth2/test/test_get_access_token.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from starlette import status 4 | 5 | from apps.oauth2.models import App, AccessToken 6 | from core.test.transaction_test_case import TransactionTestCase 7 | from core.utils.get_object_or_404 import get_object 8 | 9 | 10 | class AccessTokenTestCase(TransactionTestCase): 11 | 12 | def setUp(self): 13 | session = self.session 14 | 15 | self.app_model = App(name='App test') 16 | session.add(self.app_model) 17 | session.commit() 18 | 19 | def test_send_json(self): 20 | body = { 21 | 'client_id': self.app_model.client_id, 22 | 'client_secret': self.app_model.client_secret, 23 | 'grant_type': 'client_credentials' 24 | } 25 | 26 | headers = { 27 | 'Content-Type': 'application/json' 28 | } 29 | 30 | response = self.client.post('api/v1/token', body, headers=headers) 31 | 32 | self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) 33 | 34 | def test_send_form(self): 35 | body = { 36 | 'client_id': self.app_model.client_id, 37 | 'client_secret': self.app_model.client_secret, 38 | 'grant_type': 'client_credentials' 39 | } 40 | 41 | headers = { 42 | 'Content-Type': 'application/x-www-form-urlencoded' 43 | } 44 | 45 | response = self.client.post('api/v1/token', body, headers=headers) 46 | 47 | self.assertEqual(response.status_code, status.HTTP_200_OK) 48 | 49 | payload = response.json() 50 | 51 | access_token = get_object(self.session.query(AccessToken).filter( 52 | AccessToken.access_token == payload.get('access_token') 53 | )) 54 | 55 | self.assertIsNotNone(access_token) 56 | self.assertEqual({'access_token': access_token.access_token, 'token_type': 'bearer'}, payload) 57 | 58 | def test_send_bad_grant_type(self): 59 | body = { 60 | 'client_id': self.app_model.client_id, 61 | 'client_secret': self.app_model.client_secret, 62 | 'grant_type': 'client_credentialss' 63 | } 64 | 65 | headers = { 66 | 'Content-Type': 'application/x-www-form-urlencoded' 67 | } 68 | 69 | response = self.client.post('api/v1/token', body, headers=headers) 70 | 71 | self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) 72 | 73 | def test_send_not_exist_app(self): 74 | body = { 75 | 'client_id': f'{self.app_model.client_id[:-1]}q', 76 | 'client_secret': self.app_model.client_secret, 77 | 'grant_type': 'client_credentials' 78 | } 79 | 80 | headers = { 81 | 'Content-Type': 'application/x-www-form-urlencoded' 82 | } 83 | 84 | response = self.client.post('api/v1/token', body, headers=headers) 85 | 86 | self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) 87 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/apps/oauth2/urls.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from fastapi.routing import APIRouter 4 | from apps.oauth2.views.token_access import router as router_token_access 5 | from apps.oauth2.views.view_protected import router as router_protected 6 | 7 | router = APIRouter() 8 | 9 | router.include_router(router_token_access) 10 | router.include_router(router_protected) 11 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/apps/oauth2/views/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bergran/fast-api-project-template/ad29b3f6d37bb3653efff66cf0d8c76ce6015bb5/{{cookiecutter.project_name}}/apps/oauth2/views/__init__.py -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/apps/oauth2/views/token_access.py: -------------------------------------------------------------------------------- 1 | #  -*- coding: utf-8 -*- 2 | 3 | from fastapi import Depends, HTTPException 4 | from fastapi.routing import APIRouter 5 | from sqlalchemy.orm import Session 6 | from starlette import status 7 | 8 | from apps.oauth2.depends.app import get_app_object_access 9 | from apps.oauth2.models.access_token import AccessToken 10 | from apps.oauth2.models.apps import App 11 | from apps.oauth2.serializers.token_access import TokenAccess, TokenOut 12 | from core.config import SCOPES 13 | from core.depends import get_database 14 | 15 | router = APIRouter() 16 | 17 | 18 | @router.post('/token', response_model=TokenOut) 19 | async def login_for_access_token(app: App = Depends(get_app_object_access), 20 | session: Session = Depends(get_database), 21 | access: TokenAccess = Depends()): 22 | scopes = access.scopes 23 | 24 | if len(scopes) == 0: 25 | scopes = [list(SCOPES.keys())[0]] 26 | 27 | allowed_scopes = SCOPES.keys() 28 | 29 | for scope in scopes: 30 | if scope not in allowed_scopes: 31 | raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, 32 | detail={'scope': f'{scope} scope is not in allowed scopes'}) 33 | 34 | access_token = AccessToken(scopes=scopes) 35 | app.access_tokens.append(access_token) 36 | session.commit() 37 | 38 | return {"access_token": access_token.access_token, "token_type": "bearer"} 39 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/apps/oauth2/views/view_protected.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from fastapi import APIRouter, Depends 3 | from fastapi.encoders import jsonable_encoder 4 | 5 | from apps.oauth2.depends.app import get_app_object_scopes 6 | from apps.oauth2.models import App 7 | from apps.oauth2.serializers.app import AppOut 8 | 9 | router = APIRouter() 10 | 11 | 12 | @router.get('/me', response_model=AppOut) 13 | def me(app: App = Depends(get_app_object_scopes(['me']))): 14 | return jsonable_encoder(app) 15 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/apps/token/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bergran/fast-api-project-template/ad29b3f6d37bb3653efff66cf0d8c76ce6015bb5/{{cookiecutter.project_name}}/apps/token/__init__.py -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/apps/token/constants/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bergran/fast-api-project-template/ad29b3f6d37bb3653efff66cf0d8c76ce6015bb5/{{cookiecutter.project_name}}/apps/token/constants/__init__.py -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/apps/token/constants/jwt.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | JWT_REGEX = r'^{} [A-Za-z0-9-_=]+\.[A-Za-z0-9-_=]+\.?[A-Za-z0-9-_.+/=]*$' 4 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/apps/token/depends/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bergran/fast-api-project-template/ad29b3f6d37bb3653efff66cf0d8c76ce6015bb5/{{cookiecutter.project_name}}/apps/token/depends/__init__.py -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/apps/token/depends/get_jwt.py: -------------------------------------------------------------------------------- 1 | #  -*- coding: utf-8 -*- 2 | 3 | import re 4 | 5 | from fastapi import Header, HTTPException 6 | from starlette import status 7 | from starlette.requests import Request 8 | 9 | from apps.token.constants.jwt import JWT_REGEX 10 | 11 | 12 | def get_jwt(request: Request, authorization: str = Header('', alias='Authorization')) -> str: 13 | config = request.state.config 14 | 15 | regex = JWT_REGEX.format(config.JWT_AUTH_HEADER_PREFIX) 16 | 17 | if not re.match(regex, authorization): 18 | raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Authorization has wrong format") 19 | 20 | return authorization.split(' ')[-1] 21 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/apps/token/depends/get_token_decode.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import jwt 3 | from fastapi import Depends, HTTPException 4 | from starlette import status 5 | from starlette.requests import Request 6 | 7 | from apps.token.depends.get_jwt import get_jwt 8 | 9 | 10 | def get_token_decoded(request: Request, jwt_token: str = Depends(get_jwt)) -> str: 11 | config = request.state.config 12 | 13 | try: 14 | token = jwt.decode(jwt_token, config.JWT_SECRET_KEY, algorithms=['HS256']) 15 | except jwt.ExpiredSignatureError as ex: 16 | raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(ex)) 17 | except jwt.InvalidSignatureError as ex: 18 | raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(ex)) 19 | 20 | return token 21 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/apps/token/models/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bergran/fast-api-project-template/ad29b3f6d37bb3653efff66cf0d8c76ce6015bb5/{{cookiecutter.project_name}}/apps/token/models/__init__.py -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/apps/token/serializers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bergran/fast-api-project-template/ad29b3f6d37bb3653efff66cf0d8c76ce6015bb5/{{cookiecutter.project_name}}/apps/token/serializers/__init__.py -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/apps/token/test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bergran/fast-api-project-template/ad29b3f6d37bb3653efff66cf0d8c76ce6015bb5/{{cookiecutter.project_name}}/apps/token/test/__init__.py -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/apps/token/test/test_verify_token.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from datetime import datetime 4 | from datetime import timedelta 5 | 6 | import jwt 7 | from starlette import status 8 | 9 | from core import config 10 | from core.test.transaction_test_case import TransactionTestCase 11 | 12 | 13 | class VerifyTokenTestCase(TransactionTestCase): 14 | 15 | def setUp(self): 16 | self.token_good = self.generate_jwt(datetime.utcnow() + timedelta(minutes=30), config.SECRET_KEY) 17 | self.token_expired = self.generate_jwt(datetime.utcnow() - timedelta(minutes=30), config.SECRET_KEY) 18 | self.token_with_other_sign = self.generate_jwt(datetime.utcnow(), 'Im not secret key :)') 19 | 20 | def generate_jwt(self, exp_time, secret): 21 | return jwt.encode( 22 | {'message': 'hello world', 'exp': exp_time}, secret 23 | ) 24 | 25 | def test_with_good_token(self): 26 | token = self.token_good.decode('utf-8') 27 | 28 | response = self.client.post('api/v1/verify-token', headers={ 29 | 'Authorization': '{} {}'.format(config.JWT_AUTH_HEADER_PREFIX, token) 30 | }) 31 | 32 | self.assertEqual(response.status_code, status.HTTP_200_OK) 33 | self.assertEqual(response.json(), {'token': token}) 34 | 35 | def test_with_token_expired(self): 36 | token = self.token_expired.decode('utf-8') 37 | 38 | response = self.client.post('api/v1/verify-token', headers={ 39 | 'Authorization': '{} {}'.format(config.JWT_AUTH_HEADER_PREFIX, token) 40 | }) 41 | 42 | self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) 43 | 44 | def test_with_with_other_sign(self): 45 | token = self.token_with_other_sign.decode('utf-8') 46 | 47 | response = self.client.post('api/v1/verify-token', headers={ 48 | 'Authorization': '{} {}'.format(config.JWT_AUTH_HEADER_PREFIX, token) 49 | }) 50 | 51 | self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) 52 | 53 | def test_no_authentication_header(self): 54 | self.assertIsNotNone(self.session) 55 | response = self.client.post('api/v1/verify-token') 56 | 57 | self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) 58 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/apps/token/urls.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from fastapi.routing import APIRouter 4 | 5 | from apps.token.views.token import router as router_token 6 | 7 | 8 | router = APIRouter() 9 | router.include_router(router_token) -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/apps/token/views/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bergran/fast-api-project-template/ad29b3f6d37bb3653efff66cf0d8c76ce6015bb5/{{cookiecutter.project_name}}/apps/token/views/__init__.py -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/apps/token/views/token.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from fastapi import Depends 3 | from fastapi.routing import APIRouter 4 | from starlette import status 5 | 6 | from apps.token.depends.get_jwt import get_jwt 7 | from apps.token.depends.get_token_decode import get_token_decoded 8 | from core.serializers.message import Message 9 | 10 | router = APIRouter() 11 | 12 | 13 | @router.post('/verify-token', status_code=status.HTTP_200_OK, responses={400: {"model": Message}}) 14 | def verify_token(token: str = Depends(get_token_decoded), jwt_token: str = Depends(get_jwt)): 15 | return {'token': jwt_token} 16 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/conftest.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | 4 | import pytest 5 | from sqlalchemy import create_engine 6 | from sqlalchemy.orm import sessionmaker 7 | from sqlalchemy_utils import database_exists, drop_database, create_database 8 | from starlette.testclient import TestClient 9 | 10 | logger = logging.getLogger('fixture') 11 | logger.setLevel(logging.INFO) 12 | 13 | 14 | def pytest_sessionstart(session): 15 | os.environ.setdefault('TEST_RUN', '1') 16 | from main import app 17 | _create_database(app.config.SQLALCHEMY_DATABASE_URI) 18 | _create_database_connection(app) 19 | 20 | 21 | def pytest_sessionfinish(session): 22 | os.environ.setdefault('TEST_RUN', '1') 23 | from main import app 24 | _delete_database(app.config.SQLALCHEMY_DATABASE_URI) 25 | 26 | 27 | def _create_database(dsn): 28 | database_name = dsn.split('/')[-1] 29 | logger.info('check database {}'.format(database_name)) 30 | 31 | db_exists = database_exists(dsn) 32 | if db_exists: 33 | logger.warning('drop old database {}'.format(database_name)) 34 | drop_database(dsn) 35 | 36 | logger.info('create new database {}'.format(database_name)) 37 | create_database(dsn) 38 | 39 | 40 | @pytest.fixture(scope="class") 41 | def get_session_and_client_fixture(request): 42 | from main import app 43 | # Set TEST_RUN environment to tell app that we are running under test environment to connect dummy test 44 | os.environ.setdefault('TEST_RUN', '1') 45 | 46 | from core.db.base import Base 47 | 48 | engine = create_engine(app.config.SQLALCHEMY_DATABASE_URI) 49 | session = sessionmaker(autocommit=False, autoflush=True, bind=engine)() 50 | 51 | request.cls.session = session 52 | request.cls.client = TestClient(app) 53 | request.cls.Base = Base 54 | 55 | 56 | def _delete_database(dsn): 57 | database_name = dsn.split('/')[-1] 58 | logger.info('drop old database {}'.format(database_name)) 59 | drop_database(dsn) 60 | 61 | 62 | def _create_database_connection(app): 63 | from core.db.base import Base 64 | 65 | engine = create_engine(app.config.SQLALCHEMY_DATABASE_URI) 66 | session = sessionmaker(autocommit=False, autoflush=True, bind=engine)() 67 | 68 | import subprocess 69 | subprocess.call("cd {} && sh scripts/migrate.sh head".format(app.config.BASE_DIR), shell=True) 70 | return session, Base 71 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/core/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bergran/fast-api-project-template/ad29b3f6d37bb3653efff66cf0d8c76ce6015bb5/{{cookiecutter.project_name}}/core/__init__.py -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/core/config.py: -------------------------------------------------------------------------------- 1 | import os 2 | from datetime import timedelta 3 | 4 | 5 | class Config: 6 | TEST = { 7 | 'database': 'test_default', 8 | } 9 | 10 | _DATABASES = { 11 | 'type': '', 12 | 'username': '', 13 | 'password': '', 14 | 'host': '', 15 | 'port': '', 16 | 'database': '', 17 | } 18 | 19 | 20 | def getenv_boolean(var_name, default_value=False): 21 | result = default_value 22 | env_value = os.getenv(var_name) 23 | if env_value is not None: 24 | result = env_value.upper() in ('TRUE', '1') 25 | return result 26 | 27 | 28 | # ~~~~~ PATH ~~~~~ 29 | 30 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 31 | 32 | # ~~~~~ TEST ~~~~~ 33 | 34 | TEST_RUN = getenv_boolean('TEST_RUN', False) 35 | 36 | # ~~~~~ API ~~~~~ 37 | 38 | 39 | # ~~~~~ SECRET ~~~~~ 40 | SECRET_KEY = os.getenv('SECRET_KEY', 'cuerno de unicornio :D') 41 | 42 | if not SECRET_KEY: 43 | SECRET_KEY = os.urandom(32) 44 | 45 | # ~~~~~ APPS ~~~~~ 46 | APPS = [ 47 | 'health_check', 48 | 'token', 49 | 'hello_world' 50 | ] 51 | 52 | # ~~~~~ JWT ~~~~~ 53 | JWT_EXPIRATION_DELTA = timedelta(hours=int(os.getenv('ACCESS_TOKEN_EXPIRE_MINUTES', 10))) # in hours 54 | JWT_REFRESH_EXPIRATION_DELTA = timedelta(hours=int(os.getenv('JWT_REFRESH_EXPIRATION_DELTA', 10))) # in hours 55 | JWT_AUTH_HEADER_PREFIX = os.getenv('JWT_AUTH_HEADER_PREFIX', 'JWT') 56 | JWT_SECRET_KEY = SECRET_KEY 57 | 58 | # ~~~~~ CORS ~~~~~ 59 | 60 | BACKEND_CORS_ORIGINS = os.getenv( 61 | 'BACKEND_CORS_ORIGINS' 62 | ) # a string of origins separated by commas, e.g: 'http://localhost, http://localhost:4200, http://localhost:3000 63 | 64 | # ~~~~~ APP ~~~~~ 65 | PROJECT_NAME = os.getenv('PROJECT_NAME', 'Fastapi') 66 | 67 | # ~~~~~ EMAIL ~~~~~ 68 | SENTRY_DSN = os.getenv('SENTRY_DSN') 69 | 70 | SMTP_TLS = getenv_boolean('SMTP_TLS', True) 71 | SMTP_PORT = None 72 | _SMTP_PORT = os.getenv('SMTP_PORT') 73 | 74 | if _SMTP_PORT is not None: 75 | SMTP_PORT = int(_SMTP_PORT) 76 | 77 | SMTP_HOST = os.getenv('SMTP_HOST') 78 | SMTP_USER = os.getenv('SMTP_USER') 79 | SMTP_PASSWORD = os.getenv('SMTP_PASSWORD') 80 | 81 | EMAILS_FROM_EMAIL = os.getenv('EMAILS_FROM_EMAIL') 82 | EMAILS_FROM_NAME = PROJECT_NAME 83 | EMAIL_RESET_TOKEN_EXPIRE_HOURS = 48 84 | EMAIL_TEMPLATES_DIR = '/app/app/email-templates/build' 85 | EMAILS_ENABLED = SMTP_HOST and SMTP_PORT and EMAILS_FROM_EMAIL 86 | 87 | # ~~~~~ DATA_BASE ~~~~~ 88 | 89 | DATABASES = { 90 | 'type': os.environ.get('type', 'postgresql'), 91 | 'database': os.environ.get('database', 'fastapi'), 92 | 'username': os.environ.get('username', 'myproject'), 93 | 'password': os.environ.get('password', 'myproject'), 94 | 'host': os.environ.get('host', 'localhost'), 95 | 'port': os.environ.get('port', 5432) 96 | } 97 | 98 | 99 | # ~~~~~ OAUTH 2 ~~~~~ 100 | 101 | SCOPES = { 102 | 'read': 'Read', 103 | 'write': 'Write' 104 | } 105 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/core/db/base.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from sqlalchemy.ext.declarative import declarative_base, declared_attr 3 | 4 | 5 | class CustomBase: 6 | # Generate __tablename__ automatically 7 | @declared_attr 8 | def __tablename__(cls): 9 | return cls.__name__.lower() 10 | 11 | 12 | Base = declarative_base(cls=CustomBase) 13 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/core/db/setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from sqlalchemy import create_engine 3 | from sqlalchemy.orm import sessionmaker 4 | 5 | from core.utils.init_db import get_dsn 6 | 7 | 8 | def setup_database(app_config): 9 | dsn = get_dsn(app_config) 10 | engine = create_engine(dsn) 11 | session = sessionmaker(autocommit=False, autoflush=False, bind=engine) 12 | return session, dsn 13 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/core/depends/__init__.py: -------------------------------------------------------------------------------- 1 | from core.depends.get_database import get_database 2 | 3 | __all__ = ['get_database'] 4 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/core/depends/get_config.py: -------------------------------------------------------------------------------- 1 | #  -*- coding: utf-8 -*- 2 | from starlette.requests import Request 3 | 4 | 5 | def get_config(request: Request): 6 | return request.state.config 7 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/core/depends/get_database.py: -------------------------------------------------------------------------------- 1 | #  -*- coding: utf-8 -*- 2 | from starlette.requests import Request 3 | 4 | 5 | def get_database(request: Request): 6 | return request.state.db 7 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/core/depends/get_host_remote.py: -------------------------------------------------------------------------------- 1 | #  -*- coding: utf-8 -*- 2 | from fastapi import Header, HTTPException 3 | from starlette import status 4 | 5 | 6 | def get_remote_host( 7 | host: str = Header(None), 8 | x_real_ip: str = Header(None, alias='X-Real-IP'), 9 | scheme: str = Header('http') 10 | ): 11 | template_host = '{}://{}' 12 | if x_real_ip: 13 | return template_host.format(scheme, x_real_ip) 14 | elif host: 15 | return template_host.format(scheme, host) 16 | else: 17 | raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail='X-Real-Ip header is not set') 18 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/core/depends/get_object.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from fastapi import Path, Depends, HTTPException 3 | from sqlalchemy.orm import Session 4 | from starlette import status 5 | 6 | from core.db.base import Base 7 | from core.depends import get_database 8 | 9 | 10 | def get_object(model: Base, pk: str): 11 | def wrap( 12 | obj_pk: int = Path(..., alias = pk), 13 | db: Session = Depends(get_database) 14 | ): 15 | obj = db.query(model).filter(getattr(model, pk) == obj_pk).first() 16 | if obj is None: 17 | raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Item not found") 18 | 19 | return obj 20 | 21 | return wrap 22 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/core/middlewares/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bergran/fast-api-project-template/ad29b3f6d37bb3653efff66cf0d8c76ce6015bb5/{{cookiecutter.project_name}}/core/middlewares/__init__.py -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/core/middlewares/database.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from starlette.requests import Request 4 | from starlette.responses import Response 5 | 6 | from core.db.setup import setup_database 7 | 8 | 9 | def database_middleware(app): 10 | app_config = app.config 11 | session, dsn = setup_database(app_config) 12 | 13 | app_config.SQLALCHEMY_DATABASE_URI = dsn 14 | 15 | async def db_session_middleware(request: Request, call_next): 16 | response = Response("Internal server error", status_code=500) 17 | try: 18 | request.state.db = session() 19 | response = await call_next(request) 20 | finally: 21 | request.state.db.rollback() 22 | request.state.db.close() 23 | return response 24 | 25 | return db_session_middleware 26 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/core/middlewares/settings.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from starlette.requests import Request 3 | from starlette.responses import Response 4 | 5 | from core.utils.init_config import init_config 6 | 7 | 8 | def settings_middleware(app): 9 | config_obj = init_config() 10 | 11 | if config_obj.SECRET_KEY is None: 12 | raise RuntimeError('You have to set SECRET_KEY in the config module') 13 | 14 | app.config = config_obj 15 | 16 | async def settings_add_app(request: Request, call_next): 17 | response = Response("Internal server error", status_code=500) 18 | try: 19 | request.state.config = config_obj 20 | response = await call_next(request) 21 | finally: 22 | pass 23 | return response 24 | 25 | return settings_add_app 26 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/core/serializers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bergran/fast-api-project-template/ad29b3f6d37bb3653efff66cf0d8c76ce6015bb5/{{cookiecutter.project_name}}/core/serializers/__init__.py -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/core/serializers/message.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel 2 | 3 | 4 | class Message(BaseModel): 5 | message: str 6 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/core/test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bergran/fast-api-project-template/ad29b3f6d37bb3653efff66cf0d8c76ce6015bb5/{{cookiecutter.project_name}}/core/test/__init__.py -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/core/test/transaction_test_case.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | 3 | import pytest 4 | 5 | 6 | @pytest.mark.usefixtures("get_session_and_client_fixture") 7 | class TransactionTestCase(TestCase): 8 | 9 | def tearDown(self): 10 | meta = self.Base.metadata 11 | for table in reversed(meta.sorted_tables): 12 | self.session.execute(table.delete()) 13 | self.session.commit() 14 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/core/urls.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from fastapi.routing import APIRouter 4 | 5 | from apps.health_check.urls import router as router_health_check 6 | from apps.hello_world.urls import router as router_hello_world 7 | from apps.token.urls import router as router_token 8 | from apps.oauth2.urls import router as router_oauth2 9 | 10 | 11 | router = APIRouter() 12 | router.include_router(router_health_check, prefix='/api/v1', tags=['health_check']) 13 | router.include_router(router_hello_world, prefix='/api/v1', tags=['hello_work']) 14 | router.include_router(router_token, prefix='/api/v1', tags=['token']) 15 | router.include_router(router_oauth2, prefix='/api/v1', tags=['oauth2']) 16 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/core/utils/get_object_or_404.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from fastapi import HTTPException 3 | from sqlalchemy.orm.exc import MultipleResultsFound, NoResultFound 4 | from starlette import status 5 | 6 | 7 | def get_object_or_404(qs): 8 | try: 9 | return qs.one() 10 | except MultipleResultsFound: 11 | raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR) 12 | except NoResultFound: 13 | raise HTTPException(status_code=status.HTTP_404_NOT_FOUND) 14 | 15 | 16 | def get_object(qs): 17 | try: 18 | return qs.one() 19 | except MultipleResultsFound: 20 | return None 21 | except NoResultFound: 22 | return None 23 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/core/utils/init_config.py: -------------------------------------------------------------------------------- 1 | from core import config 2 | 3 | 4 | def init_config(): 5 | config_obj = config.Config() 6 | [ 7 | setattr(config_obj, variable, getattr(config, variable, '')) 8 | for variable in dir(config) if 9 | not variable.startswith("__") 10 | ] 11 | return config_obj 12 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/core/utils/init_db.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | def get_dsn(config): 5 | assert config is not None, 'Need to inject ConfigMiddleware' 6 | 7 | databases = config._DATABASES.copy() 8 | database_user = config.DATABASES.copy() 9 | 10 | if config.TEST_RUN: 11 | database_user.update(config.TEST) 12 | 13 | for key, value in database_user.items(): 14 | if value: 15 | databases[key] = value 16 | 17 | SQLALCHEMY_DATABASE_URI = '{type}://{username}:{password}@{host}:{port}/{database}'.format( 18 | **databases 19 | ) 20 | 21 | return SQLALCHEMY_DATABASE_URI 22 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/deployments/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.1' 2 | 3 | services: 4 | 5 | db: 6 | container_name: fastapi-db 7 | image: postgres 8 | environment: 9 | POSTGRES_PASSWORD: fastapi 10 | POSTGRES_USER: fastapi 11 | POSTGRES_DB: fastapi 12 | 13 | app: 14 | container_name: fastapi-app 15 | image: python:3.7.3-stretch 16 | working_dir: /code 17 | environment: 18 | database: fastapi 19 | username: fastapi 20 | password: fastapi 21 | host: db 22 | PYTHONPATH: /code 23 | ports: 24 | - 8000:8000 25 | depends_on: 26 | - db 27 | volumes: 28 | - ../:/code 29 | command: sh deployments/run.sh -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/deployments/run.sh: -------------------------------------------------------------------------------- 1 | pip install -r requirements.txt && \ 2 | sh scripts/migrate.sh head && \ 3 | sh scripts/runserver.sh 4 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/main.py: -------------------------------------------------------------------------------- 1 | import uvicorn 2 | from fastapi import FastAPI 3 | from fastapi.exceptions import RequestValidationError 4 | from starlette.middleware.cors import CORSMiddleware 5 | from starlette.responses import PlainTextResponse 6 | 7 | from core import config 8 | from core import urls 9 | from core.middlewares.database import database_middleware 10 | from core.middlewares.settings import settings_middleware 11 | 12 | app = FastAPI(title=config.PROJECT_NAME, openapi_url="/api/v1/openapi.json") 13 | 14 | # CORS 15 | origins = [] 16 | 17 | # Set all CORS enabled origins 18 | 19 | if config.BACKEND_CORS_ORIGINS: 20 | origins_raw = config.BACKEND_CORS_ORIGINS.split(",") 21 | for origin in origins_raw: 22 | use_origin = origin.strip() 23 | origins.append(use_origin) 24 | app.add_middleware( 25 | CORSMiddleware, 26 | allow_origins=origins, 27 | allow_credentials=True, 28 | allow_methods=["*"], 29 | allow_headers=["*"], 30 | ) 31 | 32 | 33 | app.include_router(urls.router) 34 | 35 | app.middleware('http')(settings_middleware(app)) 36 | app.middleware('http')(database_middleware(app)) 37 | 38 | 39 | @app.exception_handler(RequestValidationError) 40 | async def validation_exception_handler(request, exc): 41 | return PlainTextResponse(str(exc), status_code=400) 42 | 43 | 44 | if __name__ == "__main__": 45 | uvicorn.run(app=app, host="127.0.0.1", port=8000, log_level="info", reload=True) 46 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/requirements.txt: -------------------------------------------------------------------------------- 1 | aiohttp==3.6.2 2 | alembic==1.4.1 3 | email-validator==1.0.5 4 | factory-boy==2.12.0 5 | fastapi==0.52.0 6 | psycopg2-binary==2.8.2 7 | pydantic==1.4 8 | PyJWT==1.7.1 9 | pytest==5.4.1 10 | requests==2.23.0 11 | SQLAlchemy==1.3.15 12 | SQLAlchemy-Utils==0.33.11 13 | ujson==2.0.2 14 | uvicorn==0.11.3 15 | uvloop==0.14.0 16 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/scripts/generate_dummy_data.sh: -------------------------------------------------------------------------------- 1 | echo "Add some generator script here :D" -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/scripts/makemigrations.sh: -------------------------------------------------------------------------------- 1 | read -p "Enter name revision : " name 2 | 3 | alembic revision --autogenerate -m "$name" -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/scripts/migrate.sh: -------------------------------------------------------------------------------- 1 | input_variable=$1 2 | 3 | # Refactor this pls! :D 4 | if [ $input_variable == "--help" ] || [ $input_variable == '-h' ] 5 | then 6 | echo "This is a help to use migrate command" 7 | echo "--help or -h will show help" 8 | echo "Also you can use this input to introduce revision or head" 9 | else 10 | PYTHONPATH=.; alembic upgrade $1 11 | fi -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/scripts/runserver-dev.sh: -------------------------------------------------------------------------------- 1 | uvicorn main:app --host 0.0.0.0 --reload -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/scripts/runserver.sh: -------------------------------------------------------------------------------- 1 | uvicorn main:app --host 0.0.0.0 -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/scripts/startapp.sh: -------------------------------------------------------------------------------- 1 | dirname=$1 2 | 3 | make_module() { 4 | subdirname=$1 5 | mkdir $subdirname 6 | touch $subdirname/__init__.py 7 | } 8 | 9 | if [ $# -eq 0 ] 10 | then 11 | echo "You should into parameter to create app" 12 | exit 13 | fi 14 | 15 | # Do magic 16 | APPS_DIR=apps 17 | 18 | cd $APPS_DIR 19 | mkdir $dirname 20 | 21 | cd $dirname 22 | 23 | echo """# -*- coding: utf-8 -*- 24 | 25 | from fastapi.routing import APIRouter 26 | router = APIRouter() 27 | """ > urls.py 28 | 29 | 30 | make_module views 31 | make_module serializers 32 | make_module constants 33 | make_module models 34 | make_module services 35 | make_module tests 36 | make_module depends 37 | 38 | cd ../../ --------------------------------------------------------------------------------