Python backend for a Slack's kudos plugin.
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat(wallet): get real balance

+353 -95
+5 -4
kefi/config/__init__.py
··· 2 2 from functools import lru_cache 3 3 from importlib import import_module 4 4 5 - from pydantic import BaseSettings, PostgresDsn 5 + from pydantic import BaseSettings 6 6 7 7 8 8 class Settings(BaseSettings): 9 9 APP_NAME: str = "Kefi" 10 10 SLACK_BOT_TOKEN: str 11 - DATABASE_URL: PostgresDsn 12 - IS_TEST: bool = False 13 - DATABASE_TEST_SUFFIX: str = "_test" 11 + DATABASE_URL: str 14 12 15 13 class Config: 16 14 env_file = ".env" 15 + 16 + def db_url(self) -> str: 17 + return self.DATABASE_URL 17 18 18 19 19 20 @lru_cache()
+4 -1
kefi/config/test.py
··· 2 2 3 3 4 4 class Settings(BaseSettings): 5 - IS_TEST: bool = True 5 + DATABASE_TEST_SUFFIX: str = "_test" 6 + 7 + def db_url(self) -> str: 8 + return f"{self.DATABASE_URL}{self.DATABASE_TEST_SUFFIX}"
+3 -7
kefi/migrations/env.py
··· 9 9 10 10 # Interpret the config file for Python logging. 11 11 # This line sets up loggers basically. 12 - fileConfig(config.config_file_name) 12 + fileConfig(config.config_file_name) # type: ignore 13 13 14 14 15 15 # add your model's MetaData object here ··· 17 17 18 18 from kefi.config import settings 19 19 from kefi.models.users import User 20 + from kefi.models.wallets import Transaction 20 21 21 - if settings.IS_TEST: 22 - config.set_main_option( 23 - "sqlalchemy.url", f"{settings.DATABASE_URL}{settings.DATABASE_TEST_SUFFIX}" 24 - ) 25 - else: 26 - config.set_main_option("sqlalchemy.url", f"{settings.DATABASE_URL}") 22 + config.set_main_option("sqlalchemy.url", settings.db_url()) 27 23 target_metadata = SQLModel.metadata 28 24 29 25 # other values from the config, defined by the needs of env.py,
+61
kefi/migrations/versions/482891658252_init_models.py
··· 1 + """init models 2 + 3 + Revision ID: 482891658252 4 + Revises: 5 + Create Date: 2021-11-07 20:38:24.698282 6 + 7 + """ 8 + import sqlalchemy as sa 9 + import sqlmodel 10 + from alembic import op 11 + 12 + # revision identifiers, used by Alembic. 13 + revision = "482891658252" 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( 22 + "user", 23 + sa.Column("slack_user_id", sa.String(length=100), nullable=True), 24 + sa.Column("id", sa.Integer(), nullable=True), 25 + sa.PrimaryKeyConstraint("id"), 26 + ) 27 + op.create_index(op.f("ix_user_id"), "user", ["id"], unique=False) 28 + op.create_index( 29 + op.f("ix_user_slack_user_id"), "user", ["slack_user_id"], unique=True 30 + ) 31 + op.create_table( 32 + "transaction", 33 + sa.Column("id", sa.Integer(), nullable=True), 34 + sa.Column("amount", sa.Integer(), nullable=False), 35 + sa.Column("user_id", sa.Integer(), nullable=False), 36 + sa.ForeignKeyConstraint( 37 + ["user_id"], 38 + ["user.id"], 39 + ), 40 + sa.PrimaryKeyConstraint("id"), 41 + ) 42 + op.create_index( 43 + op.f("ix_transaction_amount"), "transaction", ["amount"], unique=False 44 + ) 45 + op.create_index(op.f("ix_transaction_id"), "transaction", ["id"], unique=False) 46 + op.create_index( 47 + op.f("ix_transaction_user_id"), "transaction", ["user_id"], unique=False 48 + ) 49 + # ### end Alembic commands ### 50 + 51 + 52 + def downgrade(): 53 + # ### commands auto generated by Alembic - please adjust! ### 54 + op.drop_index(op.f("ix_transaction_user_id"), table_name="transaction") 55 + op.drop_index(op.f("ix_transaction_id"), table_name="transaction") 56 + op.drop_index(op.f("ix_transaction_amount"), table_name="transaction") 57 + op.drop_table("transaction") 58 + op.drop_index(op.f("ix_user_slack_user_id"), table_name="user") 59 + op.drop_index(op.f("ix_user_id"), table_name="user") 60 + op.drop_table("user") 61 + # ### end Alembic commands ###
-39
kefi/migrations/versions/bdd01590a965_init_user_model.py
··· 1 - """Init User model 2 - 3 - Revision ID: bdd01590a965 4 - Revises: 5 - Create Date: 2021-11-05 16:56:27.724680 6 - 7 - """ 8 - import sqlalchemy as sa 9 - import sqlmodel 10 - from alembic import op 11 - 12 - # revision identifiers, used by Alembic. 13 - revision = "bdd01590a965" 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( 22 - "user", 23 - sa.Column("slack_user_id", sa.String(length=100), nullable=True), 24 - sa.Column("id", sa.Integer(), nullable=True), 25 - sa.PrimaryKeyConstraint("id"), 26 - ) 27 - op.create_index(op.f("ix_user_id"), "user", ["id"], unique=False) 28 - op.create_index( 29 - op.f("ix_user_slack_user_id"), "user", ["slack_user_id"], unique=True 30 - ) 31 - # ### end Alembic commands ### 32 - 33 - 34 - def downgrade(): 35 - # ### commands auto generated by Alembic - please adjust! ### 36 - op.drop_index(op.f("ix_user_slack_user_id"), table_name="user") 37 - op.drop_index(op.f("ix_user_id"), table_name="user") 38 - op.drop_table("user") 39 - # ### end Alembic commands ###
+1 -1
kefi/models/__init__.py
··· 2 2 3 3 from kefi.config import settings 4 4 5 - engine = create_engine(settings.DATABASE_URL) 5 + engine = create_engine(settings.db_url())
+22 -3
kefi/models/users.py
··· 1 - from typing import Optional 1 + from typing import TYPE_CHECKING, List, Optional, Tuple 2 2 3 3 from sqlalchemy import Column, String 4 - from sqlmodel import Field, SQLModel 4 + from sqlmodel import Field, Relationship, Session, SQLModel, select 5 + 6 + from kefi.dependencies import SlashCommandParams 7 + 8 + if TYPE_CHECKING: 9 + from kefi.models.wallets import Transaction 5 10 6 11 7 - class User(SQLModel, table=True): 12 + class User(SQLModel, table=True): # type: ignore 8 13 """A slack user.""" 9 14 10 15 id: Optional[int] = Field(default=None, primary_key=True) 11 16 slack_user_id: str = Field(sa_column=Column(String(100), unique=True, index=True)) 17 + transactions: List["Transaction"] = Relationship(back_populates="user") 18 + 19 + 20 + def get_or_create_from_command( 21 + command: SlashCommandParams, session: Session 22 + ) -> Tuple[User, bool]: 23 + """Obtains the balance of the user.""" 24 + query = select(User).filter(User.slack_user_id == command.user_id) 25 + user = session.exec(query).one_or_none() 26 + created = not user 27 + if not user: 28 + user = User(slack_user_id=command.user_id) 29 + session.add(user) 30 + return (user, created)
+13 -4
kefi/models/wallets.py
··· 1 - from decimal import Decimal 2 1 from typing import Optional 3 2 4 - from sqlmodel import Field, SQLModel 3 + from sqlmodel import Field, Relationship, Session, SQLModel, func, select 4 + 5 + from kefi.models.users import User 5 6 6 7 7 - class Transaction(SQLModel, table=True): 8 + class Transaction(SQLModel, table=True): # type: ignore 8 9 id: Optional[int] = Field(default=None, primary_key=True) 9 - amount: Decimal 10 + amount: int 11 + user_id: int = Field(foreign_key="user.id") 12 + user: User = Relationship(back_populates="transactions") 13 + 14 + 15 + def balance(user: User, session: Session) -> int: 16 + """Obtains the balance of the user.""" 17 + query = select(func.sum(Transaction.amount)).filter(Transaction.user == user) # type: ignore 18 + return session.exec(query).one() or 0
+5 -14
kefi/routers/commands.py
··· 1 1 import random 2 2 3 3 from fastapi import APIRouter, Depends 4 - from sqlmodel import Session, select 4 + from sqlmodel import Session 5 5 6 6 from kefi.dependencies import SlashCommandParams, get_session 7 - from kefi.models.users import User 7 + from kefi.models.users import get_or_create_from_command 8 + from kefi.models.wallets import balance 8 9 from kefi.templates import template_command_not_found, template_command_wallet 9 10 10 11 router = APIRouter() ··· 21 22 command: SlashCommandParams = Depends(SlashCommandParams), 22 23 ): 23 24 """Handle different commands.""" 24 - 25 - # Init commad to create the user 26 - if Commands.INIT in command.text.lower(): 27 - statement = select(User).where(User.slack_user_id == command.user_id) 28 - results = session.exec(statement) 29 - user = results.first() 30 - if not user: 31 - user = User(slack_user_id=command.user_id) 32 - session.add(user) 33 - session.commit() 34 - 25 + user, _ = get_or_create_from_command(command=command, session=session) 35 26 if Commands.WALLET in command.text.lower(): 36 - remaining_amount = random.randint(0, 100) 27 + remaining_amount = balance(user=user, session=session) 37 28 received_amount = random.randint(0, 100) 38 29 return template_command_wallet( 39 30 command.user_name, remaining_amount, received_amount
+23 -18
kefi/tests/conftest.py
··· 4 4 from fastapi.testclient import TestClient 5 5 from sqlalchemy_utils import create_database, database_exists, drop_database 6 6 from sqlmodel import Session, create_engine 7 + from sqlmodel.pool import StaticPool 7 8 8 9 from kefi.config import settings 9 10 from kefi.dependencies import get_session 10 11 from kefi.main import app 11 12 12 13 13 - @pytest.fixture(scope="session") 14 - def test_database_session(): 15 - """Fixtrue to get a session with the test database.""" 16 - # Adds '_test' to default database name 17 - url = f"{settings.DATABASE_URL}_test" 18 - # Creates the session 19 - engine = create_engine(url) 20 - with Session(engine) as session: 21 - yield session 22 - 23 - 24 - def run_schema_migrations(sqlalchemy_url: str) -> None: 14 + def run_migrations(url): 25 15 config = Config("alembic.ini") 26 - config.set_main_option("sqlalchemy.url", sqlalchemy_url) 16 + config.set_main_option("sqlalchemy.url", url) 27 17 command.upgrade(config, "head") 28 18 29 19 30 - @pytest.fixture(name="session") 31 - def session_fixture(test_database_session: Session): 20 + @pytest.fixture(scope="session", autouse=True) 21 + def test_database_creation(): 32 22 """Creates the tests database if not exists and runs the migration.""" 33 - url = str(test_database_session.bind.url) 23 + url = settings.db_url() 24 + # Handles the initial database 34 25 if database_exists(url): 35 26 drop_database(url) 36 27 create_database(url) 37 - run_schema_migrations(url) 38 - yield test_database_session 28 + # Run migrations 29 + run_migrations(url) 30 + # Wait until finish 31 + yield 32 + # On teardown, drops the database 33 + drop_database(url) 34 + 35 + 36 + @pytest.fixture(name="session", scope="function") 37 + def session_fixture(): 38 + """Fixtrue to get a session with the test database.""" 39 + # Creates the session 40 + engine = create_engine(settings.db_url(), poolclass=StaticPool) 41 + with Session(engine) as session: 42 + yield session 43 + session.rollback() 39 44 40 45 41 46 @pytest.fixture(name="client")
+25 -3
kefi/tests/test_commands.py
··· 1 + from decimal import Decimal 2 + 3 + import pytest 1 4 from faker import Faker 2 5 from fastapi.testclient import TestClient 6 + from sqlmodel import Session 7 + from sqlmodel.sql.expression import select 8 + 9 + from kefi.models.users import User 10 + from kefi.models.wallets import Transaction 3 11 4 12 fake = Faker() 5 13 6 14 7 - def test_init_commad(client: TestClient): 15 + def test_init_commad(session: Session, client: TestClient): 8 16 """Test command /kefi init""" 9 17 10 18 response = client.post( ··· 25 33 }, 26 34 ) 27 35 assert response.status_code == 200 36 + users = session.exec(select(User)).all() 37 + assert len(users) == 1 28 38 29 39 30 - def test_wallet_commad(client: TestClient): 40 + def test_wallet_commad(session: Session, client: TestClient): 31 41 """Test command /kefi wallet""" 42 + 43 + user = User(slack_user_id="user_1") 44 + session.add(user) 45 + values = [10, 15, -5] 46 + for value in values: 47 + session.add(Transaction(amount=value, user=user)) 48 + 32 49 response = client.post( 33 50 "/command/", 34 51 data={ ··· 37 54 "team_domain": "team_domain", 38 55 "channel_id": "channel_id", 39 56 "channel_name": "channel_name", 40 - "user_id": "user_id", 57 + "user_id": user.slack_user_id, 41 58 "user_name": "dummy", 42 59 "command": "/kefi", 43 60 "text": "wallet", ··· 49 66 assert response.status_code == 200 50 67 data = response.json() 51 68 assert "blocks" in data 69 + assert len(data["blocks"]) == 3 70 + assert ( 71 + data["blocks"][1]["fields"][0]["text"] 72 + == f"*Pendiente gastar:*\n {sum(values)} dekas" 73 + )
+20
kefi/tests/test_models.py
··· 1 + from decimal import Decimal 2 + 3 + from sqlmodel import Session 4 + 5 + from kefi.models.users import User 6 + from kefi.models.wallets import Transaction, balance 7 + 8 + 9 + def test_balance(session: Session): 10 + """Test the balance of the wallet.""" 11 + # Creates the user 12 + user = User(slack_user_id="user_1") 13 + session.add(user) 14 + 15 + # Adds transactions 16 + values = [10, 15, -5] 17 + for value in values: 18 + session.add(Transaction(amount=value, user=user)) 19 + 20 + assert balance(user=user, session=session) == sum(values)
+170 -1
poetry.lock
··· 132 132 python-versions = "*" 133 133 134 134 [[package]] 135 + name = "backports.entry-points-selectable" 136 + version = "1.1.0" 137 + description = "Compatibility shim providing selectable entry points for older implementations" 138 + category = "dev" 139 + optional = false 140 + python-versions = ">=2.7" 141 + 142 + [package.extras] 143 + docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] 144 + testing = ["pytest (>=4.6)", "pytest-flake8", "pytest-cov", "pytest-black (>=0.3.7)", "pytest-mypy", "pytest-checkdocs (>=2.4)", "pytest-enabler (>=1.0.1)"] 145 + 146 + [[package]] 135 147 name = "black" 136 148 version = "21.10b0" 137 149 description = "The uncompromising code formatter." ··· 167 179 python-versions = "*" 168 180 169 181 [[package]] 182 + name = "cfgv" 183 + version = "3.3.1" 184 + description = "Validate configuration and produce human readable error messages." 185 + category = "dev" 186 + optional = false 187 + python-versions = ">=3.6.1" 188 + 189 + [[package]] 170 190 name = "charset-normalizer" 171 191 version = "2.0.7" 172 192 description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." ··· 205 225 python-versions = ">=3.5" 206 226 207 227 [[package]] 228 + name = "distlib" 229 + version = "0.3.3" 230 + description = "Distribution utilities" 231 + category = "dev" 232 + optional = false 233 + python-versions = "*" 234 + 235 + [[package]] 208 236 name = "faker" 209 237 version = "9.8.0" 210 238 description = "Faker is a Python package that generates fake data for you." ··· 235 263 test = ["pytest (>=6.2.4,<7.0.0)", "pytest-cov (>=2.12.0,<4.0.0)", "mypy (==0.910)", "flake8 (>=3.8.3,<4.0.0)", "black (==21.9b0)", "isort (>=5.0.6,<6.0.0)", "requests (>=2.24.0,<3.0.0)", "httpx (>=0.14.0,<0.19.0)", "email_validator (>=1.1.1,<2.0.0)", "sqlalchemy (>=1.3.18,<1.5.0)", "peewee (>=3.13.3,<4.0.0)", "databases[sqlite] (>=0.3.2,<0.6.0)", "orjson (>=3.2.1,<4.0.0)", "ujson (>=4.0.1,<5.0.0)", "python-multipart (>=0.0.5,<0.0.6)", "flask (>=1.1.2,<3.0.0)", "anyio[trio] (>=3.2.1,<4.0.0)", "types-ujson (==0.1.1)", "types-orjson (==3.6.0)", "types-dataclasses (==0.1.7)"] 236 264 237 265 [[package]] 266 + name = "filelock" 267 + version = "3.3.2" 268 + description = "A platform independent file lock." 269 + category = "dev" 270 + optional = false 271 + python-versions = ">=3.6" 272 + 273 + [package.extras] 274 + docs = ["furo (>=2021.8.17b43)", "sphinx (>=4.1)", "sphinx-autodoc-typehints (>=1.12)"] 275 + testing = ["covdefaults (>=1.2.0)", "coverage (>=4)", "pytest (>=4)", "pytest-cov", "pytest-timeout (>=1.4.2)"] 276 + 277 + [[package]] 238 278 name = "greenlet" 239 279 version = "1.1.2" 240 280 description = "Lightweight in-process concurrent programming" ··· 276 316 python-versions = ">=3.6" 277 317 278 318 [[package]] 319 + name = "identify" 320 + version = "2.3.4" 321 + description = "File identification library for Python" 322 + category = "dev" 323 + optional = false 324 + python-versions = ">=3.6.1" 325 + 326 + [package.extras] 327 + license = ["editdistance-s"] 328 + 329 + [[package]] 279 330 name = "idna" 280 331 version = "3.3" 281 332 description = "Internationalized Domain Names in Applications (IDNA)" ··· 428 479 python-versions = "*" 429 480 430 481 [[package]] 482 + name = "nodeenv" 483 + version = "1.6.0" 484 + description = "Node.js virtual environment builder" 485 + category = "dev" 486 + optional = false 487 + python-versions = "*" 488 + 489 + [[package]] 431 490 name = "packaging" 432 491 version = "21.2" 433 492 description = "Core utilities for Python packages" ··· 502 561 testing = ["pytest", "pytest-benchmark"] 503 562 504 563 [[package]] 564 + name = "pre-commit" 565 + version = "2.15.0" 566 + description = "A framework for managing and maintaining multi-language pre-commit hooks." 567 + category = "dev" 568 + optional = false 569 + python-versions = ">=3.6.1" 570 + 571 + [package.dependencies] 572 + cfgv = ">=2.0.0" 573 + identify = ">=1.0.0" 574 + nodeenv = ">=0.11.1" 575 + pyyaml = ">=5.1" 576 + toml = "*" 577 + virtualenv = ">=20.0.8" 578 + 579 + [[package]] 505 580 name = "prompt-toolkit" 506 581 version = "3.0.22" 507 582 description = "Library for building powerful interactive command lines in Python" ··· 648 723 649 724 [package.dependencies] 650 725 six = ">=1.4.0" 726 + 727 + [[package]] 728 + name = "pyyaml" 729 + version = "6.0" 730 + description = "YAML parser and emitter for Python" 731 + category = "dev" 732 + optional = false 733 + python-versions = ">=3.6" 651 734 652 735 [[package]] 653 736 name = "regex" ··· 872 955 standard = ["websockets (>=9.1)", "httptools (>=0.2.0,<0.3.0)", "watchgod (>=0.6)", "python-dotenv (>=0.13)", "PyYAML (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "colorama (>=0.4)"] 873 956 874 957 [[package]] 958 + name = "virtualenv" 959 + version = "20.10.0" 960 + description = "Virtual Python Environment builder" 961 + category = "dev" 962 + optional = false 963 + python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" 964 + 965 + [package.dependencies] 966 + "backports.entry-points-selectable" = ">=1.0.4" 967 + distlib = ">=0.3.1,<1" 968 + filelock = ">=3.2,<4" 969 + platformdirs = ">=2,<3" 970 + six = ">=1.9.0,<2" 971 + 972 + [package.extras] 973 + docs = ["proselint (>=0.10.2)", "sphinx (>=3)", "sphinx-argparse (>=0.2.5)", "sphinx-rtd-theme (>=0.4.3)", "towncrier (>=21.3)"] 974 + testing = ["coverage (>=4)", "coverage-enable-subprocess (>=1)", "flaky (>=3)", "pytest (>=4)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.1)", "pytest-mock (>=2)", "pytest-randomly (>=1)", "pytest-timeout (>=1)", "packaging (>=20.0)"] 975 + 976 + [[package]] 875 977 name = "wcwidth" 876 978 version = "0.2.5" 877 979 description = "Measures the displayed width of unicode strings in a terminal" ··· 890 992 [metadata] 891 993 lock-version = "1.1" 892 994 python-versions = "^3.9" 893 - content-hash = "af355514354b1d2bf8d342ccf57bf99d27dad492d84658c87b1bf85858c424d3" 995 + content-hash = "74125b57164b2a2e9ab59dbc306666f72ee8226c6ead2506b5f70063216dbff7" 894 996 895 997 [metadata.files] 896 998 aioredis = [ ··· 936 1038 backcall = [ 937 1039 {file = "backcall-0.2.0-py2.py3-none-any.whl", hash = "sha256:fbbce6a29f263178a1f7915c1940bde0ec2b2a967566fe1c65c1dfb7422bd255"}, 938 1040 {file = "backcall-0.2.0.tar.gz", hash = "sha256:5cbdbf27be5e7cfadb448baf0aa95508f91f2bbc6c6437cd9cd06e2a4c215e1e"}, 1041 + ] 1042 + "backports.entry-points-selectable" = [ 1043 + {file = "backports.entry_points_selectable-1.1.0-py2.py3-none-any.whl", hash = "sha256:a6d9a871cde5e15b4c4a53e3d43ba890cc6861ec1332c9c2428c92f977192acc"}, 1044 + {file = "backports.entry_points_selectable-1.1.0.tar.gz", hash = "sha256:988468260ec1c196dab6ae1149260e2f5472c9110334e5d51adcb77867361f6a"}, 939 1045 ] 940 1046 black = [ 941 1047 {file = "black-21.10b0-py3-none-any.whl", hash = "sha256:6eb7448da9143ee65b856a5f3676b7dda98ad9abe0f87fce8c59291f15e82a5b"}, ··· 945 1051 {file = "certifi-2021.10.8-py2.py3-none-any.whl", hash = "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569"}, 946 1052 {file = "certifi-2021.10.8.tar.gz", hash = "sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872"}, 947 1053 ] 1054 + cfgv = [ 1055 + {file = "cfgv-3.3.1-py2.py3-none-any.whl", hash = "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426"}, 1056 + {file = "cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736"}, 1057 + ] 948 1058 charset-normalizer = [ 949 1059 {file = "charset-normalizer-2.0.7.tar.gz", hash = "sha256:e019de665e2bcf9c2b64e2e5aa025fa991da8720daa3c1138cadd2fd1856aed0"}, 950 1060 {file = "charset_normalizer-2.0.7-py3-none-any.whl", hash = "sha256:f7af805c321bfa1ce6714c51f254e0d5bb5e5834039bc17db7ebe3a4cec9492b"}, ··· 961 1071 {file = "decorator-5.1.0-py3-none-any.whl", hash = "sha256:7b12e7c3c6ab203a29e157335e9122cb03de9ab7264b137594103fd4a683b374"}, 962 1072 {file = "decorator-5.1.0.tar.gz", hash = "sha256:e59913af105b9860aa2c8d3272d9de5a56a4e608db9a2f167a8480b323d529a7"}, 963 1073 ] 1074 + distlib = [ 1075 + {file = "distlib-0.3.3-py2.py3-none-any.whl", hash = "sha256:c8b54e8454e5bf6237cc84c20e8264c3e991e824ef27e8f1e81049867d861e31"}, 1076 + {file = "distlib-0.3.3.zip", hash = "sha256:d982d0751ff6eaaab5e2ec8e691d949ee80eddf01a62eaa96ddb11531fe16b05"}, 1077 + ] 964 1078 faker = [ 965 1079 {file = "Faker-9.8.0-py3-none-any.whl", hash = "sha256:810182ef3597e0dfc4999a29f7cf17b99c70b361aae0f16743de6b926619ae21"}, 966 1080 {file = "Faker-9.8.0.tar.gz", hash = "sha256:22e53b8082890cca9b595ec22f9b01676b9d96c5f2f1890bcb49e4d612aa40a2"}, ··· 969 1083 {file = "fastapi-0.70.0-py3-none-any.whl", hash = "sha256:a36d5f2fad931aa3575c07a3472c784e81f3e664e3bb5c8b9c88d0ec1104f59c"}, 970 1084 {file = "fastapi-0.70.0.tar.gz", hash = "sha256:66da43cfe5185ea1df99552acffd201f1832c6b364e0f4136c0a99f933466ced"}, 971 1085 ] 1086 + filelock = [ 1087 + {file = "filelock-3.3.2-py3-none-any.whl", hash = "sha256:bb2a1c717df74c48a2d00ed625e5a66f8572a3a30baacb7657add1d7bac4097b"}, 1088 + {file = "filelock-3.3.2.tar.gz", hash = "sha256:7afc856f74fa7006a289fd10fa840e1eebd8bbff6bffb69c26c54a0512ea8cf8"}, 1089 + ] 972 1090 greenlet = [ 973 1091 {file = "greenlet-1.1.2-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:58df5c2a0e293bf665a51f8a100d3e9956febfbf1d9aaf8c0677cf70218910c6"}, 974 1092 {file = "greenlet-1.1.2-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:aec52725173bd3a7b56fe91bc56eccb26fbdff1386ef123abb63c84c5b43b63a"}, ··· 1071 1189 {file = "hiredis-2.0.0-pp37-pypy37_pp73-manylinux2010_x86_64.whl", hash = "sha256:cb2126603091902767d96bcb74093bd8b14982f41809f85c9b96e519c7e1dc41"}, 1072 1190 {file = "hiredis-2.0.0-pp37-pypy37_pp73-win32.whl", hash = "sha256:f52010e0a44e3d8530437e7da38d11fb822acfb0d5b12e9cd5ba655509937ca0"}, 1073 1191 {file = "hiredis-2.0.0.tar.gz", hash = "sha256:81d6d8e39695f2c37954d1011c0480ef7cf444d4e3ae24bc5e89ee5de360139a"}, 1192 + ] 1193 + identify = [ 1194 + {file = "identify-2.3.4-py2.py3-none-any.whl", hash = "sha256:4de55a93e0ba72bf917c840b3794eb1055a67272a1732351c557c88ec42011b1"}, 1195 + {file = "identify-2.3.4.tar.gz", hash = "sha256:595283a1c3a078ac5774ad4dc4d1bdd0c1602f60bcf11ae673b64cb2b1945762"}, 1074 1196 ] 1075 1197 idna = [ 1076 1198 {file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"}, ··· 1213 1335 {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, 1214 1336 {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, 1215 1337 ] 1338 + nodeenv = [ 1339 + {file = "nodeenv-1.6.0-py2.py3-none-any.whl", hash = "sha256:621e6b7076565ddcacd2db0294c0381e01fd28945ab36bcf00f41c5daf63bef7"}, 1340 + {file = "nodeenv-1.6.0.tar.gz", hash = "sha256:3ef13ff90291ba2a4a7a4ff9a979b63ffdd00a464dbe04acf0ea6471517a4c2b"}, 1341 + ] 1216 1342 packaging = [ 1217 1343 {file = "packaging-21.2-py3-none-any.whl", hash = "sha256:14317396d1e8cdb122989b916fa2c7e9ca8e2be9e8060a6eff75b6b7b4d8a7e0"}, 1218 1344 {file = "packaging-21.2.tar.gz", hash = "sha256:096d689d78ca690e4cd8a89568ba06d07ca097e3306a4381635073ca91479966"}, ··· 1241 1367 {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, 1242 1368 {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, 1243 1369 ] 1370 + pre-commit = [ 1371 + {file = "pre_commit-2.15.0-py2.py3-none-any.whl", hash = "sha256:a4ed01000afcb484d9eb8d504272e642c4c4099bbad3a6b27e519bd6a3e928a6"}, 1372 + {file = "pre_commit-2.15.0.tar.gz", hash = "sha256:3c25add78dbdfb6a28a651780d5c311ac40dd17f160eb3954a0c59da40a505a7"}, 1373 + ] 1244 1374 prompt-toolkit = [ 1245 1375 {file = "prompt_toolkit-3.0.22-py3-none-any.whl", hash = "sha256:48d85cdca8b6c4f16480c7ce03fd193666b62b0a21667ca56b4bb5ad679d1170"}, 1246 1376 {file = "prompt_toolkit-3.0.22.tar.gz", hash = "sha256:449f333dd120bd01f5d296a8ce1452114ba3a71fae7288d2f0ae2c918764fa72"}, ··· 1337 1467 ] 1338 1468 python-multipart = [ 1339 1469 {file = "python-multipart-0.0.5.tar.gz", hash = "sha256:f7bb5f611fc600d15fa47b3974c8aa16e93724513b49b5f95c81e6624c83fa43"}, 1470 + ] 1471 + pyyaml = [ 1472 + {file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"}, 1473 + {file = "PyYAML-6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c"}, 1474 + {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc"}, 1475 + {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b"}, 1476 + {file = "PyYAML-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5"}, 1477 + {file = "PyYAML-6.0-cp310-cp310-win32.whl", hash = "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513"}, 1478 + {file = "PyYAML-6.0-cp310-cp310-win_amd64.whl", hash = "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a"}, 1479 + {file = "PyYAML-6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86"}, 1480 + {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f"}, 1481 + {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92"}, 1482 + {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4"}, 1483 + {file = "PyYAML-6.0-cp36-cp36m-win32.whl", hash = "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293"}, 1484 + {file = "PyYAML-6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57"}, 1485 + {file = "PyYAML-6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c"}, 1486 + {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0"}, 1487 + {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4"}, 1488 + {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9"}, 1489 + {file = "PyYAML-6.0-cp37-cp37m-win32.whl", hash = "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737"}, 1490 + {file = "PyYAML-6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d"}, 1491 + {file = "PyYAML-6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b"}, 1492 + {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba"}, 1493 + {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34"}, 1494 + {file = "PyYAML-6.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287"}, 1495 + {file = "PyYAML-6.0-cp38-cp38-win32.whl", hash = "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78"}, 1496 + {file = "PyYAML-6.0-cp38-cp38-win_amd64.whl", hash = "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07"}, 1497 + {file = "PyYAML-6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b"}, 1498 + {file = "PyYAML-6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174"}, 1499 + {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803"}, 1500 + {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3"}, 1501 + {file = "PyYAML-6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0"}, 1502 + {file = "PyYAML-6.0-cp39-cp39-win32.whl", hash = "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb"}, 1503 + {file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"}, 1504 + {file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"}, 1340 1505 ] 1341 1506 regex = [ 1342 1507 {file = "regex-2021.11.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:897c539f0f3b2c3a715be651322bef2167de1cdc276b3f370ae81a3bda62df71"}, ··· 1487 1652 uvicorn = [ 1488 1653 {file = "uvicorn-0.15.0-py3-none-any.whl", hash = "sha256:17f898c64c71a2640514d4089da2689e5db1ce5d4086c2d53699bf99513421c1"}, 1489 1654 {file = "uvicorn-0.15.0.tar.gz", hash = "sha256:d9a3c0dd1ca86728d3e235182683b4cf94cd53a867c288eaeca80ee781b2caff"}, 1655 + ] 1656 + virtualenv = [ 1657 + {file = "virtualenv-20.10.0-py2.py3-none-any.whl", hash = "sha256:4b02e52a624336eece99c96e3ab7111f469c24ba226a53ec474e8e787b365814"}, 1658 + {file = "virtualenv-20.10.0.tar.gz", hash = "sha256:576d05b46eace16a9c348085f7d0dc8ef28713a2cabaa1cf0aea41e8f12c9218"}, 1490 1659 ] 1491 1660 wcwidth = [ 1492 1661 {file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"},
+1
pyproject.toml
··· 29 29 pytest-env = "^0.6.2" 30 30 SQLAlchemy-Utils = "^0.37.9" 31 31 Faker = "^9.8.0" 32 + pre-commit = "^2.15.0" 32 33 33 34 [tool.isort] 34 35 multi_line_output = 3