├── .gitignore ├── README.md └── open-lessons ├── abc-and-proto-13.05.2025 ├── .python-version ├── README.md ├── callback_protocol.py ├── file.txt ├── interfaces │ ├── __init__.py │ ├── __main__.py │ ├── fan.py │ ├── light_bulb.py │ ├── power_switch.py │ └── switchable.py ├── main.py ├── protocols │ ├── __init__.py │ └── example_readable.py ├── pyproject.toml └── uv.lock ├── decorators.06.03.2024 ├── decorators.ipynb └── requirements.txt ├── decorators.15.02.2022 ├── main.ipynb └── requirements.txt ├── decorators.25.01.2023 ├── main.ipynb └── requirements.txt ├── decorators.27.07.2023 ├── main.ipynb └── requirements.txt ├── decorators.27.12.2023 ├── main.ipynb └── requirements.txt ├── decorators.30.10.2024 ├── example_0.py ├── main.ipynb ├── main.py ├── pyproject.toml └── requirements.txt ├── django-cbv-25.09 ├── README.md ├── poetry.lock ├── pyproject.toml └── task_manager │ ├── manage.py │ ├── task_manager │ ├── __init__.py │ ├── asgi.py │ ├── settings.py │ ├── urls.py │ └── wsgi.py │ ├── tasks │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── migrations │ │ ├── 0001_initial.py │ │ ├── 0002_task_description.py │ │ └── __init__.py │ ├── models.py │ ├── templates │ │ └── tasks │ │ │ ├── base.html │ │ │ ├── task_detail.html │ │ │ ├── task_form.html │ │ │ └── task_list.html │ ├── tests.py │ ├── urls.py │ └── views.py │ └── templates │ ├── about.html │ ├── base.html │ └── index.html ├── fastapi-and-jinja-27.06 ├── README.md ├── create_fastapi_app.py ├── main.py ├── requirements.txt ├── run_main.py ├── templates │ ├── base.html │ ├── index.html │ └── products │ │ ├── details.html │ │ └── list.html ├── utils │ ├── __init__.py │ └── templating.py └── views │ ├── __init__.py │ ├── index.py │ └── products │ ├── __init__.py │ ├── crud.py │ ├── schemas.py │ └── views.py ├── fastapi-bg-tasks-20.05.2025 ├── .python-version ├── README.md ├── api │ ├── __init__.py │ └── api_v1 │ │ ├── __init__.py │ │ └── users │ │ ├── __init__.py │ │ ├── schemas.py │ │ ├── utils │ │ ├── __init__.py │ │ ├── email_senders.py │ │ └── welcome_email_sender.py │ │ └── views.py ├── app.py ├── config.py ├── docker-compose.yaml ├── pyproject.toml └── uv.lock ├── fastapi-intro-27.02.2025 ├── main.py ├── requirements.txt └── views │ ├── __init__.py │ └── movies │ ├── __init__.py │ ├── crud.py │ ├── schema.py │ └── views.py ├── fastapi-jsonapi.03.06.2024 ├── config.py ├── db.py ├── main.py ├── models │ ├── __init__.py │ ├── database.py │ ├── user.py │ └── user_bio.py ├── requirements.txt ├── schemas │ ├── __init__.py │ ├── user.py │ └── user_bio.py └── urls.py ├── fastapi.24.10.2023 ├── main.py ├── requirements.txt └── views │ ├── __init__.py │ ├── calc │ ├── __init__.py │ ├── crud.py │ ├── schemas.py │ └── views.py │ └── items │ ├── __init__.py │ ├── schemas.py │ └── views.py ├── flask-.21.02.2023 ├── main.py ├── requirements.txt ├── templates │ ├── base.html │ ├── hello.html │ ├── index.html │ └── products │ │ ├── create.html │ │ ├── details.html │ │ └── list.html ├── views │ ├── __init__.py │ └── products.py └── wsgi.py ├── flask-jsonapi.19.08.2021 ├── app.py ├── config.py ├── models │ ├── __init__.py │ ├── computer.py │ ├── database.py │ └── person.py ├── requirements.txt ├── schemas │ ├── __init__.py │ ├── computer.py │ └── person.py └── views │ ├── __init__.py │ ├── computer.py │ └── person.py ├── flask-jsonapi.20.08.2021 ├── app.py ├── config.py ├── models │ ├── __init__.py │ ├── computer.py │ ├── database.py │ └── person.py ├── requirements.txt ├── schemas │ ├── __init__.py │ ├── computer.py │ └── person.py └── views │ ├── __init__.py │ ├── computer.py │ ├── permissions │ ├── __init__.py │ ├── base.py │ ├── computer.py │ └── person.py │ └── person.py ├── flask.23.05.2022 ├── main.py ├── requirements.txt ├── templates │ ├── base.html │ └── products │ │ ├── add.html │ │ ├── details.html │ │ └── list.html └── views │ ├── __init__.py │ └── products.py ├── helper-funcs.09.02.2023 ├── main.ipynb └── requirements.txt ├── helper-funcs.21.02.2022 ├── main.ipynb └── requirements.txt ├── interfaces-and-protocols-27.08 ├── file.txt ├── interfaces │ ├── __init__.py │ ├── light_bulb.py │ ├── power_switch.py │ ├── switchable.py │ └── wireless_mouse.py ├── main.py ├── protocols │ ├── __init__.py │ ├── example_message_receiver.py │ ├── example_readable.py │ └── example_runtime_checkable_protocol.py └── requirements.txt ├── oop.18.08.2022 ├── step_1.py ├── step_2.py ├── step_3.py ├── step_4.py └── step_5.py ├── oop.25.04.2023 ├── step0.py ├── step1.py ├── step2.py └── step3.py ├── oop.25.08.2022 ├── step_1.py ├── step_2.py ├── step_3.py ├── step_4.py ├── step_5.py └── step_6.py ├── oop.base.11.06.2020 ├── multi_inheritance.py ├── people.py ├── shapes.py └── vehicles.py ├── pb-and-grpc.07.11.2024 ├── README.md ├── address-book.txt ├── client_hello.py ├── common.py ├── demo_pb_address_book.py ├── demo_pb_hello.py ├── hello.txt ├── pb │ ├── __init__.py │ ├── address_book_pb2.py │ ├── address_book_pb2.pyi │ ├── hello_pb2.py │ ├── hello_pb2.pyi │ └── hello_pb2_grpc.py ├── poetry.lock ├── protos │ ├── address-book.proto │ └── hello.proto ├── pyproject.toml └── server_hello.py └── pytest.18.11.2020 ├── api.py ├── solver.py ├── test_api.py └── test_solver.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 98 | __pypackages__/ 99 | 100 | # Celery stuff 101 | celerybeat-schedule 102 | celerybeat.pid 103 | 104 | # SageMath parsed files 105 | *.sage.py 106 | 107 | # Environments 108 | .env 109 | .venv 110 | env/ 111 | venv/ 112 | ENV/ 113 | env.bak/ 114 | venv.bak/ 115 | 116 | # Spyder project settings 117 | .spyderproject 118 | .spyproject 119 | 120 | # Rope project settings 121 | .ropeproject 122 | 123 | # mkdocs documentation 124 | /site 125 | 126 | # mypy 127 | .mypy_cache/ 128 | .dmypy.json 129 | dmypy.json 130 | 131 | # Pyre type checker 132 | .pyre/ 133 | 134 | # pytype static type analyzer 135 | .pytype/ 136 | 137 | # Cython debug symbols 138 | cython_debug/ 139 | 140 | .idea/ 141 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Base Python 2 | 3 | ### Открытые уроки: 4 | - [Основы ООП от 11.06.2020](open-lessons/oop.base.11.06.2020/) 5 | - [Введение в автотесты, pytest от 18.11.2020](open-lessons/pytest.18.11.2020/) 6 | - [Быстрая разработка JSON API приложений на Flask (день 1) от 19.08.2021](open-lessons/flask-jsonapi.19.08.2021/) 7 | - [Быстрая разработка JSON API приложений на Flask (день 2) от 20.08.2021](open-lessons/flask-jsonapi.20.08.2021/) 8 | - [Декораторы в Python от 15.02.2022](open-lessons/decorators.15.02.2022/) 9 | - [Функции-помощники: map, filter, reduce от 21.02.2022](open-lessons/helper-funcs.21.02.2022/) 10 | - [Знакомство с веб-разработкой на Flask от 23.05.2022](open-lessons/flask.23.05.2022/) 11 | - [Основы ООП на Python от 18.08.2022](open-lessons/oop.18.08.2022/) 12 | - [Продвинутый ООП и исключения в Python от 25.08.2022](open-lessons/oop.25.08.2022/) 13 | - [Декораторы в Python от 25.01.2023](open-lessons/decorators.25.01.2023/) 14 | - [Функции-помощники: map, filter, reduce от 09.02.2023](open-lessons/helper-funcs.09.02.2023/) 15 | - [Знакомство с веб разработкой на Flask 21.02.2023](open-lessons/flask-.21.02.2023/) 16 | - [Основы ООП в Python от 25.04.2023](open-lessons/oop.25.04.2023/) 17 | - [Знакомство с декораторами, декораторы для классов, декораторы методов. Занятие от 27.07.2023](open-lessons/decorators.27.07.2023/) 18 | - [Знакомство с FastAPI. Занятие от 24.10.2023](open-lessons/fastapi.24.10.2023/) 19 | - [Знакомство с декораторами. Занятие от 27.12.2023](open-lessons/decorators.27.12.2023/) 20 | - [Декораторы в Python. Занятие от 06.03.2024](open-lessons/decorators.06.03.2024/) 21 | - [FastAPI-JSON:API. Занятие от 03.06.2024](open-lessons/fastapi-jsonapi.03.06.2024/) 22 | - [Шаблоны страниц Jinja в FastAPI приложении. Занятие от 27.06.2024](open-lessons/fastapi-and-jinja-27.06/) 23 | - [Интерфейсы и протоколы в Python. Занятие от 27.08.2024](open-lessons/interfaces-and-protocols-27.08/) 24 | - [Django Class-Based Views. Занятие от 25.09.2024](open-lessons/django-cbv-25.09/) 25 | - [Декораторы в Python и как их аннотировать. Занятие от 30.10.2024](open-lessons/decorators.30.10.2024/) 26 | - [Знакомство с Protobuf и gRPC. Работа с gRPC на Python. Занятие от 07.11.2024](open-lessons/pb-and-grpc.07.11.2024/) 27 | - [Знакомство с FastAPI. Занятие от 27.02.2025](open-lessons/fastapi-intro-27.02.2025/) 28 | - [Абстрактные классы и протоколы в Python. Занятие от 13.05.2025](open-lessons/abc-and-proto-13.05.2025/) 29 | - [Фоновые задачи в FastAPI — Background Tasks. Занятие от 20.05.2025](open-lessons/fastapi-bg-tasks-20.05.2025/) 30 | 31 | 32 | ### Курсы Python Basic: 33 | 34 | - [PythonBasic.2020-07](https://github.com/OtusTeam/BasePython/tree/PythonBasic.2020-07) 35 | - [PythonBasic.2020-11](https://github.com/OtusTeam/BasePython/tree/PythonBasic.2020-11) 36 | - [PythonBasic.2021-02](https://github.com/OtusTeam/BasePython/tree/PythonBasic.2021-02) 37 | - [PythonBasic.2021-05](https://github.com/OtusTeam/BasePython/tree/PythonBasic.2021-05) 38 | - [PythonBasic.2021-08](https://github.com/OtusTeam/BasePython/tree/PythonBasic.2021-08) 39 | - [PythonBasic.2021-11](https://github.com/OtusTeam/BasePython/tree/PythonBasic.2021-11) 40 | - [PythonBasic.2022-02](https://github.com/OtusTeam/BasePython/tree/PythonBasic.2022-02) 41 | - [PythonBasic.2022-05](https://github.com/OtusTeam/BasePython/tree/PythonBasic.2022-05) 42 | - [PythonBasic.2022-08](https://github.com/OtusTeam/BasePython/tree/PythonBasic.2022-08) 43 | - [PythonBasic.2022-11](https://github.com/OtusTeam/BasePython/tree/PythonBasic.2022-11) 44 | - [PythonBasic.2023-02](https://github.com/OtusTeam/BasePython/tree/PythonBasic.2023-02) 45 | - [PythonBasic.2023-05](https://github.com/OtusTeam/BasePython/tree/PythonBasic.2023-05) 46 | - [PythonBasic.2023-08](https://github.com/OtusTeam/BasePython/tree/PythonBasic.2023-08) 47 | - [PythonBasic.2023-11](https://github.com/OtusTeam/BasePython/tree/PythonBasic.2023-11) 48 | - [PythonBasic.2024-01](https://github.com/OtusTeam/BasePython/tree/PythonBasic.2024-01) 49 | - [PythonBasic.2024-03](https://github.com/OtusTeam/BasePython/tree/PythonBasic.2024-03) 50 | - [PythonBasic.2024-05](https://github.com/OtusTeam/BasePython/tree/PythonBasic.2024-05) 51 | - [PythonBasic.2024-07](https://github.com/OtusTeam/BasePython/tree/PythonBasic.2024-07) 52 | - [PythonBasic.2024-09](https://github.com/OtusTeam/BasePython/tree/PythonBasic.2024-09) 53 | - [PythonBasic.2024-11](https://github.com/OtusTeam/BasePython/tree/PythonBasic.2024-11) 54 | - [PythonBasic.2025-01](https://github.com/OtusTeam/BasePython/tree/PythonBasic.2025-01) 55 | - [PythonBasic.2025-03](https://github.com/OtusTeam/BasePython/tree/PythonBasic.2025-03) 56 | - [PythonBasic.2025-05](https://github.com/OtusTeam/BasePython/tree/PythonBasic.2025-05) 57 | -------------------------------------------------------------------------------- /open-lessons/abc-and-proto-13.05.2025/.python-version: -------------------------------------------------------------------------------- 1 | 3.13 2 | -------------------------------------------------------------------------------- /open-lessons/abc-and-proto-13.05.2025/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OtusTeam/BasePython/d87ee93755f4c2dbb4c2b4bbd35743a2c6e44043/open-lessons/abc-and-proto-13.05.2025/README.md -------------------------------------------------------------------------------- /open-lessons/abc-and-proto-13.05.2025/callback_protocol.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from typing import Any, Callable, Protocol 3 | 4 | 5 | def configure_logging( 6 | level: int = logging.INFO, 7 | ) -> None: 8 | logging.basicConfig( 9 | level=level, 10 | datefmt="%Y-%m-%d %H:%M:%S", 11 | format="%(module)10s:%(lineno)-3d %(levelname)-7s - %(message)s", 12 | # format="[%(asctime)s.%(msecs)03d] %(module)10s:%(lineno)-3d %(levelname)-7s - %(message)s", 13 | ) 14 | 15 | 16 | log = logging.getLogger(__name__) 17 | 18 | 19 | class MessageCallbackProtocol(Protocol): 20 | __name__: str 21 | 22 | def __call__(self, message: str, sender_id: int, **kwargs: str) -> None: ... 23 | 24 | 25 | def process_message( 26 | message: str, 27 | sender_id: int, 28 | meta: dict[str, str], 29 | callback: MessageCallbackProtocol, 30 | ) -> None: 31 | log.info( 32 | "processing message: %s from sender #%d with meta %s", 33 | message, 34 | sender_id, 35 | meta, 36 | ) 37 | ... 38 | log.info("Calling callback %s", callback.__name__) 39 | callback(message, sender_id, **meta) 40 | 41 | 42 | def handle_message( 43 | message: str, 44 | sender_id: int, 45 | **kwargs: str, 46 | ) -> None: 47 | log.info("processing message: %s from sender #%d", message, sender_id) 48 | 49 | 50 | def main() -> None: 51 | configure_logging() 52 | process_message( 53 | "some message", 54 | sender_id=1, 55 | meta={"foo": "bar"}, 56 | callback=handle_message, 57 | ) 58 | 59 | 60 | if __name__ == "__main__": 61 | main() 62 | -------------------------------------------------------------------------------- /open-lessons/abc-and-proto-13.05.2025/file.txt: -------------------------------------------------------------------------------- 1 | example text file 2 | -------------------------------------------------------------------------------- /open-lessons/abc-and-proto-13.05.2025/interfaces/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OtusTeam/BasePython/d87ee93755f4c2dbb4c2b4bbd35743a2c6e44043/open-lessons/abc-and-proto-13.05.2025/interfaces/__init__.py -------------------------------------------------------------------------------- /open-lessons/abc-and-proto-13.05.2025/interfaces/__main__.py: -------------------------------------------------------------------------------- 1 | from interfaces.light_bulb import LightBulb 2 | from interfaces.fan import Fan 3 | from interfaces.power_switch import PowerSwitch 4 | 5 | 6 | def main() -> None: 7 | fan_switch = PowerSwitch(Fan()) 8 | fan_switch.toggle() 9 | light_switch = PowerSwitch(LightBulb()) 10 | light_switch.toggle() 11 | light_switch.toggle() 12 | fan_switch.toggle() 13 | light_switch.toggle() 14 | 15 | 16 | if __name__ == "__main__": 17 | main() 18 | -------------------------------------------------------------------------------- /open-lessons/abc-and-proto-13.05.2025/interfaces/fan.py: -------------------------------------------------------------------------------- 1 | from interfaces.switchable import Switchable 2 | 3 | 4 | class Fan(Switchable): 5 | def on(self) -> None: 6 | print("turned on fan", self) 7 | 8 | def off(self) -> None: 9 | print("turned off fan", self) 10 | -------------------------------------------------------------------------------- /open-lessons/abc-and-proto-13.05.2025/interfaces/light_bulb.py: -------------------------------------------------------------------------------- 1 | from interfaces.switchable import Switchable 2 | 3 | 4 | class LightBulb(Switchable): 5 | def on(self) -> None: 6 | print("turned on light bulb", self) 7 | 8 | def off(self) -> None: 9 | print("turned off light bulb", self) 10 | -------------------------------------------------------------------------------- /open-lessons/abc-and-proto-13.05.2025/interfaces/power_switch.py: -------------------------------------------------------------------------------- 1 | from interfaces.switchable import Switchable 2 | 3 | 4 | class PowerSwitch: 5 | def __init__(self, client: Switchable) -> None: 6 | self.client = client 7 | self.on = False 8 | self.client.off() 9 | 10 | def toggle(self) -> None: 11 | if self.on: 12 | self.client.off() 13 | else: 14 | self.client.on() 15 | 16 | self.on = not self.on 17 | -------------------------------------------------------------------------------- /open-lessons/abc-and-proto-13.05.2025/interfaces/switchable.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | 3 | 4 | class Switchable(ABC): 5 | @abstractmethod 6 | def on(self) -> None: 7 | raise NotImplementedError 8 | 9 | @abstractmethod 10 | def off(self) -> None: 11 | raise NotImplementedError 12 | -------------------------------------------------------------------------------- /open-lessons/abc-and-proto-13.05.2025/main.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | from typing import Protocol 3 | 4 | import requests 5 | 6 | 7 | class AbstractUser(Protocol): 8 | username: str 9 | email: str | None 10 | age: int 11 | 12 | def increase_age(self) -> None: ... 13 | 14 | 15 | class User(AbstractUser): 16 | 17 | def __init__(self, username: str, email: str | None, age: int) -> None: 18 | self.username = username 19 | self.email = email 20 | self.age = age 21 | 22 | def increase_age(self) -> None: 23 | self.age += 1 24 | 25 | 26 | class UserFetcher(ABC): 27 | @abstractmethod 28 | def fetch_users(self, count: int = 1) -> list[AbstractUser]: ... 29 | 30 | 31 | class PlainUserFetcher(UserFetcher): 32 | def fetch_users(self, count: int = 1) -> list[AbstractUser]: 33 | return [ 34 | User( 35 | username="username", 36 | email="user@email.com", 37 | age=18, 38 | ) 39 | for _ in range(count) 40 | ] 41 | 42 | 43 | class RandomUserFetcher(UserFetcher): 44 | API_URL = "https://randomuser.me/api/" 45 | 46 | def get_random_user(self) -> AbstractUser: 47 | data = requests.get(self.API_URL).json() 48 | user_data = data["results"][0] 49 | return User( 50 | username=user_data["login"]["username"], 51 | email=user_data["email"], 52 | age=user_data["dob"]["age"], 53 | ) 54 | 55 | def fetch_users(self, count: int = 1) -> list[AbstractUser]: 56 | return [self.get_random_user() for _ in range(count)] 57 | 58 | 59 | def get_and_show_users(fetcher: UserFetcher, count: int) -> None: 60 | users = fetcher.fetch_users(count=count) 61 | for user in users: 62 | print("@", user.username, user.email, user.age) 63 | user.increase_age() 64 | 65 | 66 | def main() -> None: 67 | # user = User( 68 | # username="username", 69 | # email="user@email.com", 70 | # age=18, 71 | # ) 72 | # print(user) 73 | plain_user_fetcher = PlainUserFetcher() 74 | get_and_show_users(plain_user_fetcher, count=2) 75 | random_user_fetcher = RandomUserFetcher() 76 | get_and_show_users(random_user_fetcher, count=3) 77 | 78 | 79 | if __name__ == "__main__": 80 | main() 81 | -------------------------------------------------------------------------------- /open-lessons/abc-and-proto-13.05.2025/protocols/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OtusTeam/BasePython/d87ee93755f4c2dbb4c2b4bbd35743a2c6e44043/open-lessons/abc-and-proto-13.05.2025/protocols/__init__.py -------------------------------------------------------------------------------- /open-lessons/abc-and-proto-13.05.2025/protocols/example_readable.py: -------------------------------------------------------------------------------- 1 | import random 2 | import string 3 | from typing import Protocol 4 | 5 | FILE_PATH = "/Users/suren/MyFiles/edu/OTUS/open-lessons/pb-abc-and-proto-13.05/file.txt" 6 | 7 | 8 | class Readable(Protocol): 9 | def read(self) -> bytes: ... 10 | 11 | 12 | class ApiClient(Readable): 13 | def __init__(self, url: str) -> None: 14 | self.url = url 15 | 16 | def api_call(self) -> None: 17 | print("calling api self", self) 18 | 19 | def read(self) -> bytes: 20 | return f"reading api, {self.url}".encode() 21 | 22 | 23 | class RandomReader: 24 | def read(self) -> bytes: 25 | line = str(self) + "".join(random.choices(string.ascii_letters, k=10)) 26 | return line.encode() 27 | 28 | 29 | def stream_data(filelike: Readable) -> None: 30 | print("streaming data", filelike.read()) 31 | 32 | 33 | def main() -> None: 34 | api = ApiClient("https://example.com/api-resource") 35 | 36 | random_reader = RandomReader() 37 | stream_data(api) 38 | stream_data(random_reader) 39 | 40 | with open(FILE_PATH, "rb") as file: 41 | stream_data(file) 42 | 43 | 44 | if __name__ == "__main__": 45 | main() 46 | -------------------------------------------------------------------------------- /open-lessons/abc-and-proto-13.05.2025/pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "pb-abc-and-proto-13-05" 3 | version = "0.1.0" 4 | description = "Add your description here" 5 | readme = "README.md" 6 | requires-python = ">=3.13" 7 | dependencies = [ 8 | "mypy>=1.15.0", 9 | "requests>=2.32.3", 10 | ] 11 | 12 | [tool.mypy] 13 | strict = true 14 | -------------------------------------------------------------------------------- /open-lessons/abc-and-proto-13.05.2025/uv.lock: -------------------------------------------------------------------------------- 1 | version = 1 2 | revision = 1 3 | requires-python = ">=3.13" 4 | 5 | [[package]] 6 | name = "certifi" 7 | version = "2025.4.26" 8 | source = { registry = "https://pypi.org/simple" } 9 | sdist = { url = "https://files.pythonhosted.org/packages/e8/9e/c05b3920a3b7d20d3d3310465f50348e5b3694f4f88c6daf736eef3024c4/certifi-2025.4.26.tar.gz", hash = "sha256:0a816057ea3cdefcef70270d2c515e4506bbc954f417fa5ade2021213bb8f0c6", size = 160705 } 10 | wheels = [ 11 | { url = "https://files.pythonhosted.org/packages/4a/7e/3db2bd1b1f9e95f7cddca6d6e75e2f2bd9f51b1246e546d88addca0106bd/certifi-2025.4.26-py3-none-any.whl", hash = "sha256:30350364dfe371162649852c63336a15c70c6510c2ad5015b21c2345311805f3", size = 159618 }, 12 | ] 13 | 14 | [[package]] 15 | name = "charset-normalizer" 16 | version = "3.4.2" 17 | source = { registry = "https://pypi.org/simple" } 18 | sdist = { url = "https://files.pythonhosted.org/packages/e4/33/89c2ced2b67d1c2a61c19c6751aa8902d46ce3dacb23600a283619f5a12d/charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63", size = 126367 } 19 | wheels = [ 20 | { url = "https://files.pythonhosted.org/packages/ea/12/a93df3366ed32db1d907d7593a94f1fe6293903e3e92967bebd6950ed12c/charset_normalizer-3.4.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0", size = 199622 }, 21 | { url = "https://files.pythonhosted.org/packages/04/93/bf204e6f344c39d9937d3c13c8cd5bbfc266472e51fc8c07cb7f64fcd2de/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf", size = 143435 }, 22 | { url = "https://files.pythonhosted.org/packages/22/2a/ea8a2095b0bafa6c5b5a55ffdc2f924455233ee7b91c69b7edfcc9e02284/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e", size = 153653 }, 23 | { url = "https://files.pythonhosted.org/packages/b6/57/1b090ff183d13cef485dfbe272e2fe57622a76694061353c59da52c9a659/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1", size = 146231 }, 24 | { url = "https://files.pythonhosted.org/packages/e2/28/ffc026b26f441fc67bd21ab7f03b313ab3fe46714a14b516f931abe1a2d8/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c", size = 148243 }, 25 | { url = "https://files.pythonhosted.org/packages/c0/0f/9abe9bd191629c33e69e47c6ef45ef99773320e9ad8e9cb08b8ab4a8d4cb/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691", size = 150442 }, 26 | { url = "https://files.pythonhosted.org/packages/67/7c/a123bbcedca91d5916c056407f89a7f5e8fdfce12ba825d7d6b9954a1a3c/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0", size = 145147 }, 27 | { url = "https://files.pythonhosted.org/packages/ec/fe/1ac556fa4899d967b83e9893788e86b6af4d83e4726511eaaad035e36595/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b", size = 153057 }, 28 | { url = "https://files.pythonhosted.org/packages/2b/ff/acfc0b0a70b19e3e54febdd5301a98b72fa07635e56f24f60502e954c461/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff", size = 156454 }, 29 | { url = "https://files.pythonhosted.org/packages/92/08/95b458ce9c740d0645feb0e96cea1f5ec946ea9c580a94adfe0b617f3573/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b", size = 154174 }, 30 | { url = "https://files.pythonhosted.org/packages/78/be/8392efc43487ac051eee6c36d5fbd63032d78f7728cb37aebcc98191f1ff/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148", size = 149166 }, 31 | { url = "https://files.pythonhosted.org/packages/44/96/392abd49b094d30b91d9fbda6a69519e95802250b777841cf3bda8fe136c/charset_normalizer-3.4.2-cp313-cp313-win32.whl", hash = "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7", size = 98064 }, 32 | { url = "https://files.pythonhosted.org/packages/e9/b0/0200da600134e001d91851ddc797809e2fe0ea72de90e09bec5a2fbdaccb/charset_normalizer-3.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980", size = 105641 }, 33 | { url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626 }, 34 | ] 35 | 36 | [[package]] 37 | name = "idna" 38 | version = "3.10" 39 | source = { registry = "https://pypi.org/simple" } 40 | sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 } 41 | wheels = [ 42 | { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, 43 | ] 44 | 45 | [[package]] 46 | name = "mypy" 47 | version = "1.15.0" 48 | source = { registry = "https://pypi.org/simple" } 49 | dependencies = [ 50 | { name = "mypy-extensions" }, 51 | { name = "typing-extensions" }, 52 | ] 53 | sdist = { url = "https://files.pythonhosted.org/packages/ce/43/d5e49a86afa64bd3839ea0d5b9c7103487007d728e1293f52525d6d5486a/mypy-1.15.0.tar.gz", hash = "sha256:404534629d51d3efea5c800ee7c42b72a6554d6c400e6a79eafe15d11341fd43", size = 3239717 } 54 | wheels = [ 55 | { url = "https://files.pythonhosted.org/packages/6a/9b/fd2e05d6ffff24d912f150b87db9e364fa8282045c875654ce7e32fffa66/mypy-1.15.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:93faf3fdb04768d44bf28693293f3904bbb555d076b781ad2530214ee53e3445", size = 10788592 }, 56 | { url = "https://files.pythonhosted.org/packages/74/37/b246d711c28a03ead1fd906bbc7106659aed7c089d55fe40dd58db812628/mypy-1.15.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:811aeccadfb730024c5d3e326b2fbe9249bb7413553f15499a4050f7c30e801d", size = 9753611 }, 57 | { url = "https://files.pythonhosted.org/packages/a6/ac/395808a92e10cfdac8003c3de9a2ab6dc7cde6c0d2a4df3df1b815ffd067/mypy-1.15.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:98b7b9b9aedb65fe628c62a6dc57f6d5088ef2dfca37903a7d9ee374d03acca5", size = 11438443 }, 58 | { url = "https://files.pythonhosted.org/packages/d2/8b/801aa06445d2de3895f59e476f38f3f8d610ef5d6908245f07d002676cbf/mypy-1.15.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c43a7682e24b4f576d93072216bf56eeff70d9140241f9edec0c104d0c515036", size = 12402541 }, 59 | { url = "https://files.pythonhosted.org/packages/c7/67/5a4268782eb77344cc613a4cf23540928e41f018a9a1ec4c6882baf20ab8/mypy-1.15.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:baefc32840a9f00babd83251560e0ae1573e2f9d1b067719479bfb0e987c6357", size = 12494348 }, 60 | { url = "https://files.pythonhosted.org/packages/83/3e/57bb447f7bbbfaabf1712d96f9df142624a386d98fb026a761532526057e/mypy-1.15.0-cp313-cp313-win_amd64.whl", hash = "sha256:b9378e2c00146c44793c98b8d5a61039a048e31f429fb0eb546d93f4b000bedf", size = 9373648 }, 61 | { url = "https://files.pythonhosted.org/packages/09/4e/a7d65c7322c510de2c409ff3828b03354a7c43f5a8ed458a7a131b41c7b9/mypy-1.15.0-py3-none-any.whl", hash = "sha256:5469affef548bd1895d86d3bf10ce2b44e33d86923c29e4d675b3e323437ea3e", size = 2221777 }, 62 | ] 63 | 64 | [[package]] 65 | name = "mypy-extensions" 66 | version = "1.1.0" 67 | source = { registry = "https://pypi.org/simple" } 68 | sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343 } 69 | wheels = [ 70 | { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963 }, 71 | ] 72 | 73 | [[package]] 74 | name = "pb-abc-and-proto-13-05" 75 | version = "0.1.0" 76 | source = { virtual = "." } 77 | dependencies = [ 78 | { name = "mypy" }, 79 | { name = "requests" }, 80 | ] 81 | 82 | [package.metadata] 83 | requires-dist = [ 84 | { name = "mypy", specifier = ">=1.15.0" }, 85 | { name = "requests", specifier = ">=2.32.3" }, 86 | ] 87 | 88 | [[package]] 89 | name = "requests" 90 | version = "2.32.3" 91 | source = { registry = "https://pypi.org/simple" } 92 | dependencies = [ 93 | { name = "certifi" }, 94 | { name = "charset-normalizer" }, 95 | { name = "idna" }, 96 | { name = "urllib3" }, 97 | ] 98 | sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218 } 99 | wheels = [ 100 | { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 }, 101 | ] 102 | 103 | [[package]] 104 | name = "typing-extensions" 105 | version = "4.13.2" 106 | source = { registry = "https://pypi.org/simple" } 107 | sdist = { url = "https://files.pythonhosted.org/packages/f6/37/23083fcd6e35492953e8d2aaaa68b860eb422b34627b13f2ce3eb6106061/typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef", size = 106967 } 108 | wheels = [ 109 | { url = "https://files.pythonhosted.org/packages/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c", size = 45806 }, 110 | ] 111 | 112 | [[package]] 113 | name = "urllib3" 114 | version = "2.4.0" 115 | source = { registry = "https://pypi.org/simple" } 116 | sdist = { url = "https://files.pythonhosted.org/packages/8a/78/16493d9c386d8e60e442a35feac5e00f0913c0f4b7c217c11e8ec2ff53e0/urllib3-2.4.0.tar.gz", hash = "sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466", size = 390672 } 117 | wheels = [ 118 | { url = "https://files.pythonhosted.org/packages/6b/11/cc635220681e93a0183390e26485430ca2c7b5f9d33b15c74c2861cb8091/urllib3-2.4.0-py3-none-any.whl", hash = "sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813", size = 128680 }, 119 | ] 120 | -------------------------------------------------------------------------------- /open-lessons/decorators.06.03.2024/requirements.txt: -------------------------------------------------------------------------------- 1 | anyio==4.3.0 2 | appnope==0.1.4 3 | argon2-cffi==23.1.0 4 | argon2-cffi-bindings==21.2.0 5 | arrow==1.3.0 6 | asttokens==2.4.1 7 | async-lru==2.0.4 8 | attrs==23.2.0 9 | Babel==2.14.0 10 | beautifulsoup4==4.12.3 11 | bleach==6.1.0 12 | certifi==2024.2.2 13 | cffi==1.16.0 14 | charset-normalizer==3.3.2 15 | comm==0.2.1 16 | debugpy==1.8.1 17 | decorator==5.1.1 18 | defusedxml==0.7.1 19 | executing==2.0.1 20 | fastjsonschema==2.19.1 21 | fqdn==1.5.1 22 | h11==0.14.0 23 | httpcore==1.0.4 24 | httpx==0.27.0 25 | idna==3.6 26 | ipykernel==6.29.3 27 | ipython==8.22.2 28 | isoduration==20.11.0 29 | jedi==0.19.1 30 | Jinja2==3.1.3 31 | json5==0.9.20 32 | jsonpointer==2.4 33 | jsonschema==4.21.1 34 | jsonschema-specifications==2023.12.1 35 | jupyter-events==0.9.0 36 | jupyter-lsp==2.2.4 37 | jupyter_client==8.6.0 38 | jupyter_core==5.7.1 39 | jupyter_server==2.13.0 40 | jupyter_server_terminals==0.5.2 41 | jupyterlab==4.1.3 42 | jupyterlab_pygments==0.3.0 43 | jupyterlab_server==2.25.3 44 | MarkupSafe==2.1.5 45 | matplotlib-inline==0.1.6 46 | mistune==3.0.2 47 | nbclient==0.9.0 48 | nbconvert==7.16.2 49 | nbformat==5.9.2 50 | nest-asyncio==1.6.0 51 | notebook==7.1.1 52 | notebook_shim==0.2.4 53 | overrides==7.7.0 54 | packaging==23.2 55 | pandocfilters==1.5.1 56 | parso==0.8.3 57 | pexpect==4.9.0 58 | platformdirs==4.2.0 59 | prometheus_client==0.20.0 60 | prompt-toolkit==3.0.43 61 | psutil==5.9.8 62 | ptyprocess==0.7.0 63 | pure-eval==0.2.2 64 | pycparser==2.21 65 | Pygments==2.17.2 66 | python-dateutil==2.9.0.post0 67 | python-json-logger==2.0.7 68 | PyYAML==6.0.1 69 | pyzmq==25.1.2 70 | referencing==0.33.0 71 | requests==2.31.0 72 | rfc3339-validator==0.1.4 73 | rfc3986-validator==0.1.1 74 | rpds-py==0.18.0 75 | Send2Trash==1.8.2 76 | six==1.16.0 77 | sniffio==1.3.1 78 | soupsieve==2.5 79 | stack-data==0.6.3 80 | terminado==0.18.0 81 | tinycss2==1.2.1 82 | tornado==6.4 83 | traitlets==5.14.1 84 | types-python-dateutil==2.8.19.20240106 85 | uri-template==1.3.0 86 | urllib3==2.2.1 87 | wcwidth==0.2.13 88 | webcolors==1.13 89 | webencodings==0.5.1 90 | websocket-client==1.7.0 91 | -------------------------------------------------------------------------------- /open-lessons/decorators.15.02.2022/requirements.txt: -------------------------------------------------------------------------------- 1 | appnope==0.1.2 2 | argon2-cffi==21.3.0 3 | argon2-cffi-bindings==21.2.0 4 | asttokens==2.0.5 5 | attrs==21.4.0 6 | backcall==0.2.0 7 | black==22.1.0 8 | bleach==4.1.0 9 | cffi==1.15.0 10 | click==8.0.3 11 | debugpy==1.5.1 12 | decorator==5.1.1 13 | defusedxml==0.7.1 14 | entrypoints==0.4 15 | executing==0.8.2 16 | ipykernel==6.9.1 17 | ipython==8.0.1 18 | ipython-genutils==0.2.0 19 | jedi==0.18.1 20 | Jinja2==3.0.3 21 | jsonschema==4.4.0 22 | jupyter-client==7.1.2 23 | jupyter-core==4.9.1 24 | jupyterlab-pygments==0.1.2 25 | MarkupSafe==2.0.1 26 | matplotlib-inline==0.1.3 27 | mistune==0.8.4 28 | mypy-extensions==0.4.3 29 | nbclient==0.5.11 30 | nbconvert==6.4.2 31 | nbformat==5.1.3 32 | nest-asyncio==1.5.4 33 | notebook==6.4.8 34 | packaging==21.3 35 | pandocfilters==1.5.0 36 | parso==0.8.3 37 | pathspec==0.9.0 38 | pexpect==4.8.0 39 | pickleshare==0.7.5 40 | platformdirs==2.5.0 41 | prometheus-client==0.13.1 42 | prompt-toolkit==3.0.28 43 | ptyprocess==0.7.0 44 | pure-eval==0.2.2 45 | pycparser==2.21 46 | Pygments==2.11.2 47 | pyparsing==3.0.7 48 | pyrsistent==0.18.1 49 | python-dateutil==2.8.2 50 | pyzmq==22.3.0 51 | Send2Trash==1.8.0 52 | six==1.16.0 53 | stack-data==0.2.0 54 | terminado==0.13.1 55 | testpath==0.5.0 56 | tomli==2.0.1 57 | tornado==6.1 58 | traitlets==5.1.1 59 | typing_extensions==4.1.1 60 | wcwidth==0.2.5 61 | webencodings==0.5.1 62 | -------------------------------------------------------------------------------- /open-lessons/decorators.25.01.2023/requirements.txt: -------------------------------------------------------------------------------- 1 | anyio==3.6.2 2 | appnope==0.1.3 3 | argon2-cffi==21.3.0 4 | argon2-cffi-bindings==21.2.0 5 | arrow==1.2.3 6 | asttokens==2.2.1 7 | attrs==22.2.0 8 | backcall==0.2.0 9 | beautifulsoup4==4.11.1 10 | bleach==6.0.0 11 | cffi==1.15.1 12 | comm==0.1.2 13 | debugpy==1.6.6 14 | decorator==5.1.1 15 | defusedxml==0.7.1 16 | entrypoints==0.4 17 | executing==1.2.0 18 | fastjsonschema==2.16.2 19 | fqdn==1.5.1 20 | idna==3.4 21 | ipykernel==6.20.2 22 | ipython==8.8.0 23 | ipython-genutils==0.2.0 24 | ipywidgets==8.0.4 25 | isoduration==20.11.0 26 | jedi==0.18.2 27 | Jinja2==3.1.2 28 | jsonpointer==2.3 29 | jsonschema==4.17.3 30 | jupyter==1.0.0 31 | jupyter-console==6.4.4 32 | jupyter-events==0.6.3 33 | jupyter_client==7.4.9 34 | jupyter_core==5.1.5 35 | jupyter_server==2.1.0 36 | jupyter_server_terminals==0.4.4 37 | jupyterlab-pygments==0.2.2 38 | jupyterlab-widgets==3.0.5 39 | MarkupSafe==2.1.2 40 | matplotlib-inline==0.1.6 41 | mistune==2.0.4 42 | nbclassic==0.4.8 43 | nbclient==0.7.2 44 | nbconvert==7.2.9 45 | nbformat==5.7.3 46 | nest-asyncio==1.5.6 47 | notebook==6.5.2 48 | notebook_shim==0.2.2 49 | packaging==23.0 50 | pandocfilters==1.5.0 51 | parso==0.8.3 52 | pexpect==4.8.0 53 | pickleshare==0.7.5 54 | platformdirs==2.6.2 55 | prometheus-client==0.16.0 56 | prompt-toolkit==3.0.36 57 | psutil==5.9.4 58 | ptyprocess==0.7.0 59 | pure-eval==0.2.2 60 | pycparser==2.21 61 | Pygments==2.14.0 62 | pyrsistent==0.19.3 63 | python-dateutil==2.8.2 64 | python-json-logger==2.0.4 65 | PyYAML==6.0 66 | pyzmq==25.0.0 67 | qtconsole==5.4.0 68 | QtPy==2.3.0 69 | rfc3339-validator==0.1.4 70 | rfc3986-validator==0.1.1 71 | Send2Trash==1.8.0 72 | six==1.16.0 73 | sniffio==1.3.0 74 | soupsieve==2.3.2.post1 75 | stack-data==0.6.2 76 | terminado==0.17.1 77 | tinycss2==1.2.1 78 | tornado==6.2 79 | traitlets==5.8.1 80 | uri-template==1.2.0 81 | wcwidth==0.2.6 82 | webcolors==1.12 83 | webencodings==0.5.1 84 | websocket-client==1.4.2 85 | widgetsnbextension==4.0.5 86 | -------------------------------------------------------------------------------- /open-lessons/decorators.27.07.2023/requirements.txt: -------------------------------------------------------------------------------- 1 | anyio==3.7.1 2 | appnope==0.1.3 3 | argon2-cffi==21.3.0 4 | argon2-cffi-bindings==21.2.0 5 | arrow==1.2.3 6 | asttokens==2.2.1 7 | async-lru==2.0.3 8 | attrs==23.1.0 9 | Babel==2.12.1 10 | backcall==0.2.0 11 | beautifulsoup4==4.12.2 12 | bleach==6.0.0 13 | certifi==2023.7.22 14 | cffi==1.15.1 15 | charset-normalizer==3.2.0 16 | comm==0.1.3 17 | debugpy==1.6.7 18 | decorator==5.1.1 19 | defusedxml==0.7.1 20 | executing==1.2.0 21 | fastjsonschema==2.18.0 22 | fqdn==1.5.1 23 | idna==3.4 24 | ipykernel==6.25.0 25 | ipython==8.14.0 26 | isoduration==20.11.0 27 | jedi==0.18.2 28 | Jinja2==3.1.2 29 | json5==0.9.14 30 | jsonpointer==2.4 31 | jsonschema==4.18.4 32 | jsonschema-specifications==2023.7.1 33 | jupyter-events==0.6.3 34 | jupyter-lsp==2.2.0 35 | jupyter_client==8.3.0 36 | jupyter_core==5.3.1 37 | jupyter_server==2.7.0 38 | jupyter_server_terminals==0.4.4 39 | jupyterlab==4.0.3 40 | jupyterlab-pygments==0.2.2 41 | jupyterlab_server==2.24.0 42 | MarkupSafe==2.1.3 43 | matplotlib-inline==0.1.6 44 | mistune==3.0.1 45 | nbclient==0.8.0 46 | nbconvert==7.7.3 47 | nbformat==5.9.1 48 | nest-asyncio==1.5.6 49 | notebook==7.0.0 50 | notebook_shim==0.2.3 51 | overrides==7.3.1 52 | packaging==23.1 53 | pandocfilters==1.5.0 54 | parso==0.8.3 55 | pexpect==4.8.0 56 | pickleshare==0.7.5 57 | platformdirs==3.9.1 58 | prometheus-client==0.17.1 59 | prompt-toolkit==3.0.39 60 | psutil==5.9.5 61 | ptyprocess==0.7.0 62 | pure-eval==0.2.2 63 | pycparser==2.21 64 | Pygments==2.15.1 65 | python-dateutil==2.8.2 66 | python-json-logger==2.0.7 67 | PyYAML==6.0.1 68 | pyzmq==25.1.0 69 | referencing==0.30.0 70 | requests==2.31.0 71 | rfc3339-validator==0.1.4 72 | rfc3986-validator==0.1.1 73 | rpds-py==0.9.2 74 | Send2Trash==1.8.2 75 | six==1.16.0 76 | sniffio==1.3.0 77 | soupsieve==2.4.1 78 | stack-data==0.6.2 79 | terminado==0.17.1 80 | tinycss2==1.2.1 81 | tornado==6.3.2 82 | traitlets==5.9.0 83 | uri-template==1.3.0 84 | urllib3==2.0.4 85 | wcwidth==0.2.6 86 | webcolors==1.13 87 | webencodings==0.5.1 88 | websocket-client==1.6.1 89 | -------------------------------------------------------------------------------- /open-lessons/decorators.27.12.2023/requirements.txt: -------------------------------------------------------------------------------- 1 | anyio==4.2.0 2 | appnope==0.1.3 3 | argon2-cffi==23.1.0 4 | argon2-cffi-bindings==21.2.0 5 | arrow==1.3.0 6 | asttokens==2.4.1 7 | async-lru==2.0.4 8 | attrs==23.1.0 9 | Babel==2.14.0 10 | beautifulsoup4==4.12.2 11 | bleach==6.1.0 12 | certifi==2023.11.17 13 | cffi==1.16.0 14 | charset-normalizer==3.3.2 15 | comm==0.2.0 16 | debugpy==1.8.0 17 | decorator==5.1.1 18 | defusedxml==0.7.1 19 | executing==2.0.1 20 | fastjsonschema==2.19.0 21 | fqdn==1.5.1 22 | idna==3.6 23 | ipykernel==6.28.0 24 | ipython==8.19.0 25 | isoduration==20.11.0 26 | jedi==0.19.1 27 | Jinja2==3.1.2 28 | json5==0.9.14 29 | jsonpointer==2.4 30 | jsonschema==4.20.0 31 | jsonschema-specifications==2023.12.1 32 | jupyter-events==0.9.0 33 | jupyter-lsp==2.2.1 34 | jupyter_client==8.6.0 35 | jupyter_core==5.6.0 36 | jupyter_server==2.12.1 37 | jupyter_server_terminals==0.5.1 38 | jupyterlab==4.0.9 39 | jupyterlab_pygments==0.3.0 40 | jupyterlab_server==2.25.2 41 | MarkupSafe==2.1.3 42 | matplotlib-inline==0.1.6 43 | mistune==3.0.2 44 | nbclient==0.9.0 45 | nbconvert==7.13.1 46 | nbformat==5.9.2 47 | nest-asyncio==1.5.8 48 | notebook==7.0.6 49 | notebook_shim==0.2.3 50 | overrides==7.4.0 51 | packaging==23.2 52 | pandocfilters==1.5.0 53 | parso==0.8.3 54 | pexpect==4.9.0 55 | platformdirs==4.1.0 56 | prometheus-client==0.19.0 57 | prompt-toolkit==3.0.43 58 | psutil==5.9.7 59 | ptyprocess==0.7.0 60 | pure-eval==0.2.2 61 | pycparser==2.21 62 | Pygments==2.17.2 63 | python-dateutil==2.8.2 64 | python-json-logger==2.0.7 65 | PyYAML==6.0.1 66 | pyzmq==25.1.2 67 | referencing==0.32.0 68 | requests==2.31.0 69 | rfc3339-validator==0.1.4 70 | rfc3986-validator==0.1.1 71 | rpds-py==0.15.2 72 | Send2Trash==1.8.2 73 | six==1.16.0 74 | sniffio==1.3.0 75 | soupsieve==2.5 76 | stack-data==0.6.3 77 | terminado==0.18.0 78 | tinycss2==1.2.1 79 | tornado==6.4 80 | traitlets==5.14.0 81 | types-python-dateutil==2.8.19.14 82 | uri-template==1.3.0 83 | urllib3==2.1.0 84 | wcwidth==0.2.12 85 | webcolors==1.13 86 | webencodings==0.5.1 87 | websocket-client==1.7.0 88 | -------------------------------------------------------------------------------- /open-lessons/decorators.30.10.2024/example_0.py: -------------------------------------------------------------------------------- 1 | from typing import TypeVar, reveal_type 2 | 3 | IntOrFloat = TypeVar("IntOrFloat", float, int) 4 | 5 | 6 | def cube(num: IntOrFloat) -> IntOrFloat: 7 | return num**3 8 | 9 | 10 | def main() -> None: 11 | c1 = cube(5) 12 | reveal_type(c1) 13 | print(c1) 14 | 15 | c2 = cube(2.5) 16 | reveal_type(c2) 17 | print(c2) 18 | 19 | 20 | if __name__ == '__main__': 21 | main() 22 | -------------------------------------------------------------------------------- /open-lessons/decorators.30.10.2024/main.py: -------------------------------------------------------------------------------- 1 | from functools import wraps 2 | from typing import TypeVar, reveal_type, Sized, Callable, ParamSpec 3 | 4 | IntOrFloat = TypeVar("IntOrFloat", float, int) 5 | 6 | ReturnType = TypeVar("ReturnType") 7 | P = ParamSpec("P") 8 | 9 | 10 | def announced(func: Callable[P, ReturnType]) -> Callable[P, ReturnType]: 11 | print("creating announced function", func) 12 | 13 | @wraps(func) 14 | def new_announced(*args: P.args, **kwargs: P.kwargs) -> ReturnType: 15 | print("run function", func, "args:", args, "kwargs:", kwargs) 16 | result = func(*args, **kwargs) 17 | print("done function", func, "res:", result) 18 | return result 19 | 20 | print("successfully created announced func for", func, "announced:", new_announced) 21 | return new_announced 22 | 23 | 24 | @announced 25 | def cube(num: IntOrFloat) -> IntOrFloat: 26 | return num ** 3 27 | 28 | 29 | @announced 30 | def power(num: IntOrFloat, exponent: int) -> IntOrFloat: 31 | val: IntOrFloat = num ** exponent 32 | return val 33 | 34 | 35 | @announced 36 | def many_str(line: str, times: int) -> str: 37 | return line * times 38 | 39 | 40 | @announced 41 | def get_size(obj: Sized) -> int: 42 | return len(obj) 43 | 44 | 45 | def main() -> None: 46 | c1 = cube(5) 47 | reveal_type(c1) 48 | print("print:", c1) 49 | 50 | c2 = cube(2.5) 51 | reveal_type(c2) 52 | print("print:", c2) 53 | # 54 | p = power(2, 10) 55 | reveal_type(p) 56 | print("print:", p) 57 | p2 = power(num=3, exponent=5) 58 | reveal_type(p2) 59 | print("print:", p2) 60 | line = many_str("foobar", 3) 61 | reveal_type(line) 62 | print("print:", line) 63 | size = get_size({"spam", "eggs"}) 64 | reveal_type(size) 65 | print("print:", size) 66 | 67 | 68 | if __name__ == '__main__': 69 | main() 70 | -------------------------------------------------------------------------------- /open-lessons/decorators.30.10.2024/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.mypy] 2 | strict = true 3 | -------------------------------------------------------------------------------- /open-lessons/decorators.30.10.2024/requirements.txt: -------------------------------------------------------------------------------- 1 | anyio==4.6.2.post1 2 | appnope==0.1.4 3 | argon2-cffi==23.1.0 4 | argon2-cffi-bindings==21.2.0 5 | arrow==1.3.0 6 | asttokens==2.4.1 7 | async-lru==2.0.4 8 | attrs==24.2.0 9 | babel==2.16.0 10 | beautifulsoup4==4.12.3 11 | bleach==6.2.0 12 | certifi==2024.8.30 13 | cffi==1.17.1 14 | charset-normalizer==3.4.0 15 | comm==0.2.2 16 | debugpy==1.8.7 17 | decorator==5.1.1 18 | defusedxml==0.7.1 19 | executing==2.1.0 20 | fastjsonschema==2.20.0 21 | fqdn==1.5.1 22 | h11==0.14.0 23 | httpcore==1.0.6 24 | httpx==0.27.2 25 | idna==3.10 26 | ipykernel==6.29.5 27 | ipython==8.29.0 28 | isoduration==20.11.0 29 | jedi==0.19.1 30 | Jinja2==3.1.4 31 | json5==0.9.25 32 | jsonpointer==3.0.0 33 | jsonschema==4.23.0 34 | jsonschema-specifications==2024.10.1 35 | jupyter-events==0.10.0 36 | jupyter-lsp==2.2.5 37 | jupyter_client==8.6.3 38 | jupyter_core==5.7.2 39 | jupyter_server==2.14.2 40 | jupyter_server_terminals==0.5.3 41 | jupyterlab==4.2.5 42 | jupyterlab_pygments==0.3.0 43 | jupyterlab_server==2.27.3 44 | MarkupSafe==3.0.2 45 | matplotlib-inline==0.1.7 46 | mistune==3.0.2 47 | mypy==1.13.0 48 | mypy-extensions==1.0.0 49 | nbclient==0.10.0 50 | nbconvert==7.16.4 51 | nbformat==5.10.4 52 | nest-asyncio==1.6.0 53 | notebook==7.2.2 54 | notebook_shim==0.2.4 55 | overrides==7.7.0 56 | packaging==24.1 57 | pandocfilters==1.5.1 58 | parso==0.8.4 59 | pexpect==4.9.0 60 | platformdirs==4.3.6 61 | prometheus_client==0.21.0 62 | prompt_toolkit==3.0.48 63 | psutil==6.1.0 64 | ptyprocess==0.7.0 65 | pure_eval==0.2.3 66 | pycparser==2.22 67 | Pygments==2.18.0 68 | python-dateutil==2.9.0.post0 69 | python-json-logger==2.0.7 70 | PyYAML==6.0.2 71 | pyzmq==26.2.0 72 | referencing==0.35.1 73 | requests==2.32.3 74 | rfc3339-validator==0.1.4 75 | rfc3986-validator==0.1.1 76 | rpds-py==0.20.0 77 | Send2Trash==1.8.3 78 | setuptools==75.3.0 79 | six==1.16.0 80 | sniffio==1.3.1 81 | soupsieve==2.6 82 | stack-data==0.6.3 83 | terminado==0.18.1 84 | tinycss2==1.4.0 85 | tornado==6.4.1 86 | traitlets==5.14.3 87 | types-python-dateutil==2.9.0.20241003 88 | typing_extensions==4.12.2 89 | uri-template==1.3.0 90 | urllib3==2.2.3 91 | wcwidth==0.2.13 92 | webcolors==24.8.0 93 | webencodings==0.5.1 94 | websocket-client==1.8.0 95 | -------------------------------------------------------------------------------- /open-lessons/django-cbv-25.09/README.md: -------------------------------------------------------------------------------- 1 | # Django Class Based Views Examples 2 | 3 | Init poetry: 4 | 5 | ```shell 6 | poetry init --no-interaction 7 | ``` -------------------------------------------------------------------------------- /open-lessons/django-cbv-25.09/poetry.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. 2 | 3 | [[package]] 4 | name = "asgiref" 5 | version = "3.8.1" 6 | description = "ASGI specs, helper code, and adapters" 7 | optional = false 8 | python-versions = ">=3.8" 9 | files = [ 10 | {file = "asgiref-3.8.1-py3-none-any.whl", hash = "sha256:3e1e3ecc849832fe52ccf2cb6686b7a55f82bb1d6aee72a58826471390335e47"}, 11 | {file = "asgiref-3.8.1.tar.gz", hash = "sha256:c343bd80a0bec947a9860adb4c432ffa7db769836c64238fc34bdc3fec84d590"}, 12 | ] 13 | 14 | [package.extras] 15 | tests = ["mypy (>=0.800)", "pytest", "pytest-asyncio"] 16 | 17 | [[package]] 18 | name = "django" 19 | version = "5.1.1" 20 | description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design." 21 | optional = false 22 | python-versions = ">=3.10" 23 | files = [ 24 | {file = "Django-5.1.1-py3-none-any.whl", hash = "sha256:71603f27dac22a6533fb38d83072eea9ddb4017fead6f67f2562a40402d61c3f"}, 25 | {file = "Django-5.1.1.tar.gz", hash = "sha256:021ffb7fdab3d2d388bc8c7c2434eb9c1f6f4d09e6119010bbb1694dda286bc2"}, 26 | ] 27 | 28 | [package.dependencies] 29 | asgiref = ">=3.8.1,<4" 30 | sqlparse = ">=0.3.1" 31 | tzdata = {version = "*", markers = "sys_platform == \"win32\""} 32 | 33 | [package.extras] 34 | argon2 = ["argon2-cffi (>=19.1.0)"] 35 | bcrypt = ["bcrypt"] 36 | 37 | [[package]] 38 | name = "sqlparse" 39 | version = "0.5.1" 40 | description = "A non-validating SQL parser." 41 | optional = false 42 | python-versions = ">=3.8" 43 | files = [ 44 | {file = "sqlparse-0.5.1-py3-none-any.whl", hash = "sha256:773dcbf9a5ab44a090f3441e2180efe2560220203dc2f8c0b0fa141e18b505e4"}, 45 | {file = "sqlparse-0.5.1.tar.gz", hash = "sha256:bb6b4df465655ef332548e24f08e205afc81b9ab86cb1c45657a7ff173a3a00e"}, 46 | ] 47 | 48 | [package.extras] 49 | dev = ["build", "hatch"] 50 | doc = ["sphinx"] 51 | 52 | [[package]] 53 | name = "tzdata" 54 | version = "2024.2" 55 | description = "Provider of IANA time zone data" 56 | optional = false 57 | python-versions = ">=2" 58 | files = [ 59 | {file = "tzdata-2024.2-py2.py3-none-any.whl", hash = "sha256:a48093786cdcde33cad18c2555e8532f34422074448fbc874186f0abd79565cd"}, 60 | {file = "tzdata-2024.2.tar.gz", hash = "sha256:7d85cc416e9382e69095b7bdf4afd9e3880418a2413feec7069d533d6b4e31cc"}, 61 | ] 62 | 63 | [metadata] 64 | lock-version = "2.0" 65 | python-versions = "^3.12" 66 | content-hash = "319a0ba62510a6c5bc6f5a861990a2bf9a827331daa8563ce5d8dcf426fefd39" 67 | -------------------------------------------------------------------------------- /open-lessons/django-cbv-25.09/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | package-mode = false 3 | 4 | [tool.poetry.dependencies] 5 | python = "^3.12" 6 | django = "^5.1.1" 7 | -------------------------------------------------------------------------------- /open-lessons/django-cbv-25.09/task_manager/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | """Run administrative tasks.""" 9 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "task_manager.settings") 10 | try: 11 | from django.core.management import execute_from_command_line 12 | except ImportError as exc: 13 | raise ImportError( 14 | "Couldn't import Django. Are you sure it's installed and " 15 | "available on your PYTHONPATH environment variable? Did you " 16 | "forget to activate a virtual environment?" 17 | ) from exc 18 | execute_from_command_line(sys.argv) 19 | 20 | 21 | if __name__ == "__main__": 22 | main() 23 | -------------------------------------------------------------------------------- /open-lessons/django-cbv-25.09/task_manager/task_manager/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OtusTeam/BasePython/d87ee93755f4c2dbb4c2b4bbd35743a2c6e44043/open-lessons/django-cbv-25.09/task_manager/task_manager/__init__.py -------------------------------------------------------------------------------- /open-lessons/django-cbv-25.09/task_manager/task_manager/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for task_manager project. 3 | 4 | It exposes the ASGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/5.1/howto/deployment/asgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.asgi import get_asgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "task_manager.settings") 15 | 16 | application = get_asgi_application() 17 | -------------------------------------------------------------------------------- /open-lessons/django-cbv-25.09/task_manager/task_manager/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for task_manager project. 3 | 4 | Generated by 'django-admin startproject' using Django 5.1.1. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/5.1/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/5.1/ref/settings/ 11 | """ 12 | 13 | from pathlib import Path 14 | 15 | # Build paths inside the project like this: BASE_DIR / 'subdir'. 16 | BASE_DIR = Path(__file__).resolve().parent.parent 17 | 18 | 19 | # Quick-start development settings - unsuitable for production 20 | # See https://docs.djangoproject.com/en/5.1/howto/deployment/checklist/ 21 | 22 | # SECURITY WARNING: keep the secret key used in production secret! 23 | SECRET_KEY = "django-insecure-o%@r8k&sl5@1xak!asdkm3knxwtebfk)u)xl&6(%^4vrs(g@h=" 24 | 25 | # SECURITY WARNING: don't run with debug turned on in production! 26 | DEBUG = True 27 | 28 | ALLOWED_HOSTS = [] 29 | 30 | 31 | # Application definition 32 | 33 | INSTALLED_APPS = [ 34 | "django.contrib.admin", 35 | "django.contrib.auth", 36 | "django.contrib.contenttypes", 37 | "django.contrib.sessions", 38 | "django.contrib.messages", 39 | "django.contrib.staticfiles", 40 | # 41 | "tasks.apps.TasksConfig", 42 | ] 43 | 44 | MIDDLEWARE = [ 45 | "django.middleware.security.SecurityMiddleware", 46 | "django.contrib.sessions.middleware.SessionMiddleware", 47 | "django.middleware.common.CommonMiddleware", 48 | "django.middleware.csrf.CsrfViewMiddleware", 49 | "django.contrib.auth.middleware.AuthenticationMiddleware", 50 | "django.contrib.messages.middleware.MessageMiddleware", 51 | "django.middleware.clickjacking.XFrameOptionsMiddleware", 52 | ] 53 | 54 | ROOT_URLCONF = "task_manager.urls" 55 | 56 | TEMPLATES = [ 57 | { 58 | "BACKEND": "django.template.backends.django.DjangoTemplates", 59 | "DIRS": [BASE_DIR / "templates"], 60 | "APP_DIRS": True, 61 | "OPTIONS": { 62 | "context_processors": [ 63 | "django.template.context_processors.debug", 64 | "django.template.context_processors.request", 65 | "django.contrib.auth.context_processors.auth", 66 | "django.contrib.messages.context_processors.messages", 67 | ], 68 | }, 69 | }, 70 | ] 71 | 72 | WSGI_APPLICATION = "task_manager.wsgi.application" 73 | 74 | 75 | # Database 76 | # https://docs.djangoproject.com/en/5.1/ref/settings/#databases 77 | 78 | DATABASES = { 79 | "default": { 80 | "ENGINE": "django.db.backends.sqlite3", 81 | "NAME": BASE_DIR / "db.sqlite3", 82 | } 83 | } 84 | 85 | 86 | # Password validation 87 | # https://docs.djangoproject.com/en/5.1/ref/settings/#auth-password-validators 88 | 89 | AUTH_PASSWORD_VALIDATORS = [ 90 | { 91 | "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", 92 | }, 93 | { 94 | "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", 95 | }, 96 | { 97 | "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", 98 | }, 99 | { 100 | "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", 101 | }, 102 | ] 103 | 104 | 105 | # Internationalization 106 | # https://docs.djangoproject.com/en/5.1/topics/i18n/ 107 | 108 | LANGUAGE_CODE = "en-us" 109 | 110 | TIME_ZONE = "UTC" 111 | 112 | USE_I18N = True 113 | 114 | USE_TZ = True 115 | 116 | 117 | # Static files (CSS, JavaScript, Images) 118 | # https://docs.djangoproject.com/en/5.1/howto/static-files/ 119 | 120 | STATIC_URL = "static/" 121 | 122 | # Default primary key field type 123 | # https://docs.djangoproject.com/en/5.1/ref/settings/#default-auto-field 124 | 125 | DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" 126 | -------------------------------------------------------------------------------- /open-lessons/django-cbv-25.09/task_manager/task_manager/urls.py: -------------------------------------------------------------------------------- 1 | """ 2 | URL configuration for task_manager project. 3 | 4 | The `urlpatterns` list routes URLs to views. For more information please see: 5 | https://docs.djangoproject.com/en/5.1/topics/http/urls/ 6 | Examples: 7 | Function views 8 | 1. Add an import: from my_app import views 9 | 2. Add a URL to urlpatterns: path('', views.home, name='home') 10 | Class-based views 11 | 1. Add an import: from other_app.views import Home 12 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') 13 | Including another URLconf 14 | 1. Import the include() function: from django.urls import include, path 15 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 16 | """ 17 | 18 | from django.contrib import admin 19 | from django.urls import path, include 20 | from django.views.generic import TemplateView 21 | 22 | 23 | urlpatterns = [ 24 | path("", TemplateView.as_view(template_name="index.html"), name="index"), 25 | path("about/", TemplateView.as_view(template_name="about.html"), name="about"), 26 | path("tasks/", include("tasks.urls")), 27 | path("admin/", admin.site.urls), 28 | ] 29 | -------------------------------------------------------------------------------- /open-lessons/django-cbv-25.09/task_manager/task_manager/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for task_manager project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/5.1/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "task_manager.settings") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /open-lessons/django-cbv-25.09/task_manager/tasks/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OtusTeam/BasePython/d87ee93755f4c2dbb4c2b4bbd35743a2c6e44043/open-lessons/django-cbv-25.09/task_manager/tasks/__init__.py -------------------------------------------------------------------------------- /open-lessons/django-cbv-25.09/task_manager/tasks/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from .models import Task 4 | 5 | 6 | @admin.register(Task) 7 | class TaskAdmin(admin.ModelAdmin): 8 | list_display = "id", "title", "description", "done" 9 | list_display_links = "id", "title", 10 | -------------------------------------------------------------------------------- /open-lessons/django-cbv-25.09/task_manager/tasks/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class TasksConfig(AppConfig): 5 | default_auto_field = "django.db.models.BigAutoField" 6 | name = "tasks" 7 | -------------------------------------------------------------------------------- /open-lessons/django-cbv-25.09/task_manager/tasks/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.1.1 on 2024-09-25 17:41 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | initial = True 9 | 10 | dependencies = [] 11 | 12 | operations = [ 13 | migrations.CreateModel( 14 | name="Task", 15 | fields=[ 16 | ( 17 | "id", 18 | models.BigAutoField( 19 | auto_created=True, 20 | primary_key=True, 21 | serialize=False, 22 | verbose_name="ID", 23 | ), 24 | ), 25 | ("title", models.CharField(default="", max_length=200)), 26 | ("done", models.BooleanField(default=False)), 27 | ], 28 | options={ 29 | "ordering": ("id",), 30 | }, 31 | ), 32 | ] 33 | -------------------------------------------------------------------------------- /open-lessons/django-cbv-25.09/task_manager/tasks/migrations/0002_task_description.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.1.1 on 2024-09-25 18:03 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("tasks", "0001_initial"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name="task", 15 | name="description", 16 | field=models.TextField(blank=True, db_default="", default=""), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /open-lessons/django-cbv-25.09/task_manager/tasks/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OtusTeam/BasePython/d87ee93755f4c2dbb4c2b4bbd35743a2c6e44043/open-lessons/django-cbv-25.09/task_manager/tasks/migrations/__init__.py -------------------------------------------------------------------------------- /open-lessons/django-cbv-25.09/task_manager/tasks/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.shortcuts import reverse 3 | 4 | 5 | class Task(models.Model): 6 | 7 | class Meta: 8 | ordering = ("id", ) 9 | 10 | title = models.CharField(max_length=200, default="", null=False) 11 | description = models.TextField(default="", db_default="", blank=True) 12 | done = models.BooleanField(default=False) 13 | 14 | def get_absolute_url(self): 15 | return reverse("tasks:detail", kwargs={"pk": self.pk}) 16 | 17 | def __str__(self): 18 | return self.title 19 | -------------------------------------------------------------------------------- /open-lessons/django-cbv-25.09/task_manager/tasks/templates/tasks/base.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | -------------------------------------------------------------------------------- /open-lessons/django-cbv-25.09/task_manager/tasks/templates/tasks/task_detail.html: -------------------------------------------------------------------------------- 1 | {% extends 'tasks/base.html' %} 2 | 3 | {% block title %} 4 | Task #{{ task.id }} 5 | {% endblock %} 6 | 7 | {% block body %} 8 |

Task "{{ task.title }}"

9 |
10 | Description: {{ task.description }} 11 |
12 |
13 | Done: {{ task.done }} 14 |
15 | {% endblock %} 16 | -------------------------------------------------------------------------------- /open-lessons/django-cbv-25.09/task_manager/tasks/templates/tasks/task_form.html: -------------------------------------------------------------------------------- 1 | {% extends 'tasks/base.html' %} 2 | 3 | {% block title %} 4 | Add a new task 5 | {% endblock %} 6 | 7 | {% block body %} 8 |

Create

9 |
10 | {% csrf_token %} 11 | {{ form.as_p }} 12 | 13 |
14 | {% endblock %} 15 | -------------------------------------------------------------------------------- /open-lessons/django-cbv-25.09/task_manager/tasks/templates/tasks/task_list.html: -------------------------------------------------------------------------------- 1 | {% extends 'tasks/base.html' %} 2 | 3 | {% block title %} 4 | Tasks list 5 | {% endblock %} 6 | 7 | {% block body %} 8 |

Tasks

9 | 23 | 24 |
25 | Create new 26 |
27 | {% endblock %} 28 | -------------------------------------------------------------------------------- /open-lessons/django-cbv-25.09/task_manager/tasks/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /open-lessons/django-cbv-25.09/task_manager/tasks/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from . import views 4 | 5 | app_name = "tasks" 6 | 7 | urlpatterns = [ 8 | path("", views.TasksListView.as_view(), name="list"), 9 | path("/", views.TaskDetailView.as_view(), name="detail"), 10 | path("create/", views.TaskCreateView.as_view(), name="create"), 11 | ] 12 | -------------------------------------------------------------------------------- /open-lessons/django-cbv-25.09/task_manager/tasks/views.py: -------------------------------------------------------------------------------- 1 | from django.views.generic import ( 2 | ListView, 3 | DetailView, 4 | CreateView, 5 | ) 6 | 7 | from .models import Task 8 | 9 | 10 | class TasksListView(ListView): 11 | model = Task 12 | 13 | 14 | class TaskDetailView(DetailView): 15 | model = Task 16 | 17 | 18 | class TaskCreateView(CreateView): 19 | model = Task 20 | fields = ( 21 | "title", 22 | "description", 23 | ) 24 | # template_name = "custom-template-name.html" 25 | # context_object_name = "task" 26 | # context_object_name = "assignment" 27 | -------------------------------------------------------------------------------- /open-lessons/django-cbv-25.09/task_manager/templates/about.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block title %} 4 | About 5 | {% endblock %} 6 | 7 | {% block body %} 8 |

About

9 |

It's a task manager

10 | {% endblock %} 11 | -------------------------------------------------------------------------------- /open-lessons/django-cbv-25.09/task_manager/templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {% block title %}base title{% endblock %} 6 | 7 | 8 | 21 | {% block body %} 22 | {% endblock %} 23 | 24 |
25 |
26 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /open-lessons/django-cbv-25.09/task_manager/templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block title %} 4 | Index 5 | {% endblock %} 6 | 7 | {% block body %} 8 |

Hello world!

9 | {% endblock %} 10 | -------------------------------------------------------------------------------- /open-lessons/fastapi-and-jinja-27.06/README.md: -------------------------------------------------------------------------------- 1 | Полезные материалы: 2 | - FastAPI Jinja Templates https://fastapi.tiangolo.com/reference/templating/ 3 | - Templates - FastAPI https://fastapi.tiangolo.com/advanced/templates/ 4 | - Базовое приложение на FastAPI https://github.com/mahenzon/FastAPI-base-app 5 | - Template Designer Documentation — Jinja Documentation (3.1.x) https://jinja.palletsprojects.com/en/3.1.x/templates/ 6 | - Начало работы с FastAPI - базовое приложение https://www.youtube.com/watch?v=XWJWJfTWjSs 7 | - Полное погружение в HTMX + Jinja шаблоны https://www.youtube.com/watch?v=EbBicPnQBoE 8 | - Стрим по сравнению Flask vs FastAPI vs Django https://www.youtube.com/watch?v=D20C8bBj2hY (работа с шаблонами на примере todo list) 9 | -------------------------------------------------------------------------------- /open-lessons/fastapi-and-jinja-27.06/create_fastapi_app.py: -------------------------------------------------------------------------------- 1 | """ 2 | Take a look: 3 | https://github.com/mahenzon/FastAPI-base-app 4 | """ 5 | 6 | from contextlib import asynccontextmanager 7 | 8 | from fastapi import FastAPI 9 | from fastapi.responses import ORJSONResponse 10 | from fastapi.openapi.docs import ( 11 | get_redoc_html, 12 | get_swagger_ui_html, 13 | get_swagger_ui_oauth2_redirect_html, 14 | ) 15 | 16 | 17 | @asynccontextmanager 18 | async def lifespan(app: FastAPI): 19 | # startup 20 | yield 21 | # shutdown 22 | 23 | 24 | def register_static_docs_routes(app: FastAPI): 25 | @app.get("/docs", include_in_schema=False) 26 | async def custom_swagger_ui_html(): 27 | return get_swagger_ui_html( 28 | openapi_url=app.openapi_url, 29 | title=app.title + " - Swagger UI", 30 | oauth2_redirect_url=app.swagger_ui_oauth2_redirect_url, 31 | swagger_js_url="https://unpkg.com/swagger-ui-dist@5/swagger-ui-bundle.js", 32 | swagger_css_url="https://unpkg.com/swagger-ui-dist@5/swagger-ui.css", 33 | ) 34 | 35 | @app.get(app.swagger_ui_oauth2_redirect_url, include_in_schema=False) 36 | async def swagger_ui_redirect(): 37 | return get_swagger_ui_oauth2_redirect_html() 38 | 39 | @app.get("/redoc", include_in_schema=False) 40 | async def redoc_html(): 41 | return get_redoc_html( 42 | openapi_url=app.openapi_url, 43 | title=app.title + " - ReDoc", 44 | redoc_js_url="https://unpkg.com/redoc@next/bundles/redoc.standalone.js", 45 | ) 46 | 47 | 48 | def create_app( 49 | create_custom_static_urls: bool = False, 50 | ) -> FastAPI: 51 | app = FastAPI( 52 | default_response_class=ORJSONResponse, 53 | lifespan=lifespan, 54 | docs_url=None if create_custom_static_urls else "/docs", 55 | redoc_url=None if create_custom_static_urls else "/redoc", 56 | ) 57 | if create_custom_static_urls: 58 | register_static_docs_routes(app) 59 | return app 60 | -------------------------------------------------------------------------------- /open-lessons/fastapi-and-jinja-27.06/main.py: -------------------------------------------------------------------------------- 1 | from create_fastapi_app import create_app 2 | 3 | from views.index import router as router_index 4 | from views.products import router as router_products 5 | 6 | app = create_app(create_custom_static_urls=True) 7 | 8 | app.include_router(router_index) 9 | app.include_router(router_products) 10 | -------------------------------------------------------------------------------- /open-lessons/fastapi-and-jinja-27.06/requirements.txt: -------------------------------------------------------------------------------- 1 | annotated-types==0.7.0 2 | anyio==4.4.0 3 | certifi==2024.6.2 4 | click==8.1.7 5 | dnspython==2.6.1 6 | email_validator==2.2.0 7 | fastapi==0.111.0 8 | fastapi-cli==0.0.4 9 | h11==0.14.0 10 | httpcore==1.0.5 11 | httptools==0.6.1 12 | httpx==0.27.0 13 | idna==3.7 14 | itsdangerous==2.2.0 15 | Jinja2==3.1.4 16 | markdown-it-py==3.0.0 17 | MarkupSafe==2.1.5 18 | mdurl==0.1.2 19 | orjson==3.10.5 20 | pydantic==2.7.4 21 | pydantic-extra-types==2.8.2 22 | pydantic-settings==2.3.4 23 | pydantic_core==2.18.4 24 | Pygments==2.18.0 25 | python-dotenv==1.0.1 26 | python-multipart==0.0.9 27 | PyYAML==6.0.1 28 | rich==13.7.1 29 | shellingham==1.5.4 30 | sniffio==1.3.1 31 | starlette==0.37.2 32 | typer==0.12.3 33 | typing_extensions==4.12.2 34 | ujson==5.10.0 35 | uvicorn==0.30.1 36 | uvloop==0.19.0 37 | watchfiles==0.22.0 38 | websockets==12.0 39 | -------------------------------------------------------------------------------- /open-lessons/fastapi-and-jinja-27.06/run_main.py: -------------------------------------------------------------------------------- 1 | import uvicorn 2 | 3 | if __name__ == "__main__": 4 | uvicorn.run( 5 | "main:app", 6 | host="0.0.0.0", 7 | port=8000, 8 | reload=True, 9 | ) 10 | -------------------------------------------------------------------------------- /open-lessons/fastapi-and-jinja-27.06/templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {% block title %} 6 | Base 7 | {% endblock %} 8 | 9 | 10 |
11 | {% block body %} 12 | 13 | {% endblock %} 14 |
15 | 16 | 21 | 22 | -------------------------------------------------------------------------------- /open-lessons/fastapi-and-jinja-27.06/templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block title %} 4 | Index Page 5 | {% endblock %} 6 | 7 | {% block body %} 8 |

Hello world!

9 | 10 | 11 |
12 |

Pages:

13 | 18 |
19 | {% endblock %} 20 | -------------------------------------------------------------------------------- /open-lessons/fastapi-and-jinja-27.06/templates/products/details.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block title %} 4 | Product #{{ product.id }} details 5 | {% endblock %} 6 | 7 | {% block body %} 8 |

Product info

9 | 10 |
11 |

Name: {{ product.name }}

12 |

Price: {{ product.price }}

13 |
14 | 15 |
16 | Back to products list 17 |
18 | {% endblock %} 19 | -------------------------------------------------------------------------------- /open-lessons/fastapi-and-jinja-27.06/templates/products/list.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block title %} 4 | Products list 5 | {% endblock %} 6 | 7 | {% block body %} 8 |

Products

9 | 10 | 18 | 19 |
20 | Back to main page 21 |
22 | {% endblock %} 23 | -------------------------------------------------------------------------------- /open-lessons/fastapi-and-jinja-27.06/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OtusTeam/BasePython/d87ee93755f4c2dbb4c2b4bbd35743a2c6e44043/open-lessons/fastapi-and-jinja-27.06/utils/__init__.py -------------------------------------------------------------------------------- /open-lessons/fastapi-and-jinja-27.06/utils/templating.py: -------------------------------------------------------------------------------- 1 | __all__ = ( 2 | "templates", 3 | "TemplateResponse", 4 | ) 5 | 6 | from starlette.templating import Jinja2Templates 7 | 8 | templates = Jinja2Templates(directory="templates") 9 | TemplateResponse = templates.TemplateResponse 10 | -------------------------------------------------------------------------------- /open-lessons/fastapi-and-jinja-27.06/views/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OtusTeam/BasePython/d87ee93755f4c2dbb4c2b4bbd35743a2c6e44043/open-lessons/fastapi-and-jinja-27.06/views/__init__.py -------------------------------------------------------------------------------- /open-lessons/fastapi-and-jinja-27.06/views/index.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter, Request 2 | from fastapi.responses import HTMLResponse 3 | 4 | from utils.templating import TemplateResponse 5 | 6 | router = APIRouter() 7 | 8 | 9 | @router.get( 10 | "/", 11 | name="main:index", 12 | response_class=HTMLResponse, 13 | ) 14 | def index(request: Request): 15 | return TemplateResponse( 16 | request=request, 17 | name="index.html", 18 | ) 19 | -------------------------------------------------------------------------------- /open-lessons/fastapi-and-jinja-27.06/views/products/__init__.py: -------------------------------------------------------------------------------- 1 | __all__ = ("router",) 2 | 3 | from .views import router 4 | -------------------------------------------------------------------------------- /open-lessons/fastapi-and-jinja-27.06/views/products/crud.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel 2 | 3 | from .schemas import ProductSchema, ProductCreateSchema, ProductIdType 4 | 5 | 6 | class ProductCRUD(BaseModel): 7 | id_to_product: dict[ProductIdType, ProductSchema] = {} 8 | last_id: int = 0 9 | 10 | @property 11 | def next_id(self) -> int: 12 | self.last_id += 1 13 | return self.last_id 14 | 15 | def products_list(self) -> list[ProductSchema]: 16 | return list(self.id_to_product.values()) 17 | 18 | def product_details(self, product_id: int) -> ProductSchema | None: 19 | return self.id_to_product.get(product_id) 20 | 21 | def create_product(self, product_in: ProductCreateSchema) -> ProductSchema: 22 | product = ProductSchema( 23 | id=self.next_id, 24 | **product_in.model_dump(), 25 | ) 26 | self.id_to_product[product.id] = product 27 | return product 28 | 29 | 30 | storage = ProductCRUD() 31 | 32 | storage.create_product( 33 | ProductCreateSchema( 34 | name="Desktop", 35 | price=1234, 36 | ) 37 | ) 38 | storage.create_product( 39 | ProductCreateSchema( 40 | name="Laptop", 41 | price=1122, 42 | ) 43 | ) 44 | storage.create_product( 45 | ProductCreateSchema( 46 | name="Smartphone", 47 | price=999, 48 | ) 49 | ) 50 | -------------------------------------------------------------------------------- /open-lessons/fastapi-and-jinja-27.06/views/products/schemas.py: -------------------------------------------------------------------------------- 1 | from typing import Annotated 2 | 3 | from pydantic import BaseModel, Field 4 | from annotated_types import MinLen 5 | 6 | 7 | ProductIdType = int 8 | 9 | 10 | class ProductBaseSchema(BaseModel): 11 | name: str 12 | price: int 13 | 14 | 15 | class ProductCreateSchema(ProductBaseSchema): 16 | """ 17 | Create new product w/ this data 18 | """ 19 | 20 | name: Annotated[str, MinLen(3)] 21 | 22 | 23 | # class ProductUpdateSchema(ProductBaseSchema): 24 | # pass 25 | 26 | 27 | class ProductReadSchema(ProductBaseSchema): 28 | id: ProductIdType = Field( 29 | example="1", 30 | ) 31 | name: str = Field(example="Laptop") 32 | 33 | 34 | class ProductSchema(ProductBaseSchema): 35 | id: ProductIdType 36 | -------------------------------------------------------------------------------- /open-lessons/fastapi-and-jinja-27.06/views/products/views.py: -------------------------------------------------------------------------------- 1 | from typing import Annotated 2 | 3 | from fastapi import APIRouter, Request, HTTPException, status, Form 4 | from fastapi.responses import HTMLResponse 5 | from annotated_types import Gt 6 | from starlette.responses import RedirectResponse 7 | 8 | from utils.templating import TemplateResponse 9 | 10 | from .crud import storage 11 | from .schemas import ProductCreateSchema 12 | 13 | router = APIRouter( 14 | prefix="/products", 15 | tags=["Products"], 16 | ) 17 | 18 | 19 | @router.get( 20 | "/", 21 | name="products:list", 22 | response_class=HTMLResponse, 23 | ) 24 | def get_products_list(request: Request): 25 | products_list = storage.products_list() 26 | return TemplateResponse( 27 | request=request, 28 | name="products/list.html", 29 | context={"products": products_list}, 30 | ) 31 | 32 | 33 | @router.get(path="/create/", response_class=HTMLResponse) 34 | def show_form_to_create_new_product(): 35 | # TODO 36 | pass 37 | 38 | 39 | @router.post( 40 | path="/create/", 41 | response_class=RedirectResponse, 42 | status_code=status.HTTP_303_SEE_OTHER, 43 | ) 44 | def create_new_product( 45 | name: str = Form(...), 46 | price: int = Form(...), 47 | ): 48 | product_in = ProductCreateSchema(name=name, price=price) 49 | product = storage.create_product(product_in=product_in) 50 | 51 | return router.url_path_for("products:details", product_id=product.id) 52 | 53 | 54 | @router.get( 55 | "/{product_id}/", 56 | name="products:details", 57 | response_class=HTMLResponse, 58 | ) 59 | def get_product_details( 60 | request: Request, 61 | product_id: Annotated[int, Gt(0)], 62 | ): 63 | product = storage.product_details(product_id=product_id) 64 | if product is None: 65 | raise HTTPException( 66 | status_code=status.HTTP_404_NOT_FOUND, 67 | detail=f"Product #{product_id} not found", 68 | ) 69 | return TemplateResponse( 70 | request=request, 71 | name="products/details.html", 72 | context={"product": product}, 73 | ) 74 | -------------------------------------------------------------------------------- /open-lessons/fastapi-bg-tasks-20.05.2025/.python-version: -------------------------------------------------------------------------------- 1 | 3.13 2 | -------------------------------------------------------------------------------- /open-lessons/fastapi-bg-tasks-20.05.2025/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OtusTeam/BasePython/d87ee93755f4c2dbb4c2b4bbd35743a2c6e44043/open-lessons/fastapi-bg-tasks-20.05.2025/README.md -------------------------------------------------------------------------------- /open-lessons/fastapi-bg-tasks-20.05.2025/api/__init__.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter 2 | from api.api_v1 import router as api_v1 3 | 4 | 5 | router = APIRouter( 6 | prefix="/api", 7 | ) 8 | router.include_router(api_v1) 9 | -------------------------------------------------------------------------------- /open-lessons/fastapi-bg-tasks-20.05.2025/api/api_v1/__init__.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter 2 | from api.api_v1.users.views import router as users_router 3 | 4 | router = APIRouter( 5 | prefix="/v1", 6 | ) 7 | router.include_router(users_router) 8 | -------------------------------------------------------------------------------- /open-lessons/fastapi-bg-tasks-20.05.2025/api/api_v1/users/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OtusTeam/BasePython/d87ee93755f4c2dbb4c2b4bbd35743a2c6e44043/open-lessons/fastapi-bg-tasks-20.05.2025/api/api_v1/users/__init__.py -------------------------------------------------------------------------------- /open-lessons/fastapi-bg-tasks-20.05.2025/api/api_v1/users/schemas.py: -------------------------------------------------------------------------------- 1 | from typing import Annotated 2 | from uuid import UUID 3 | 4 | from pydantic import BaseModel, EmailStr 5 | from annotated_types import Len 6 | 7 | 8 | class UserBase(BaseModel): 9 | # noinspection PyTypeHints 10 | username: Annotated[ 11 | str, 12 | Len(min_length=3, max_length=20), 13 | ] 14 | email: EmailStr 15 | full_name: str = "" 16 | 17 | 18 | class UserCreate(UserBase): 19 | """ 20 | Create user 21 | """ 22 | 23 | # noinspection PyTypeHints 24 | password: Annotated[ 25 | str, 26 | Len(min_length=8, max_length=1000), 27 | ] 28 | 29 | 30 | class UserRead(UserBase): 31 | id: UUID 32 | -------------------------------------------------------------------------------- /open-lessons/fastapi-bg-tasks-20.05.2025/api/api_v1/users/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OtusTeam/BasePython/d87ee93755f4c2dbb4c2b4bbd35743a2c6e44043/open-lessons/fastapi-bg-tasks-20.05.2025/api/api_v1/users/utils/__init__.py -------------------------------------------------------------------------------- /open-lessons/fastapi-bg-tasks-20.05.2025/api/api_v1/users/utils/email_senders.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from email.message import EmailMessage 3 | 4 | import aiosmtplib 5 | 6 | from config import ADMIN_EMAIL 7 | 8 | 9 | async def send_email( 10 | recipient: str, 11 | subject: str, 12 | body: str, 13 | ): 14 | message = EmailMessage() 15 | message["Subject"] = subject 16 | message["From"] = ADMIN_EMAIL 17 | message["To"] = recipient 18 | message.set_content(body) 19 | 20 | await aiosmtplib.send( 21 | message, 22 | sender=ADMIN_EMAIL, 23 | recipients=[recipient], 24 | hostname="localhost", 25 | port=1025, 26 | ) 27 | 28 | 29 | if __name__ == "__main__": 30 | asyncio.run( 31 | send_email( 32 | "rec@a.b", 33 | "hello", 34 | "first line\n\nanother line", 35 | ), 36 | ) 37 | -------------------------------------------------------------------------------- /open-lessons/fastapi-bg-tasks-20.05.2025/api/api_v1/users/utils/welcome_email_sender.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import logging 3 | 4 | import httpx 5 | 6 | from api.api_v1.users.schemas import UserBase 7 | from api.api_v1.users.utils.email_senders import send_email 8 | from config import LOG_DEFAULT_FORMAT 9 | 10 | log = logging.getLogger(__name__) 11 | 12 | 13 | body_template = """\ 14 | Hello, {name}! 15 | 16 | Welcome to our site! 17 | You can do here some things. 18 | 19 | Feel free to ask me any questions! 20 | 21 | Best regards, 22 | Site Admin. 23 | """ 24 | 25 | 26 | async def send_welcome_email( 27 | user: UserBase, 28 | ): 29 | log.info("Sending welcome email to %s", user) 30 | # # НИКОГДА не делаем синхронных вызовов в асинк коде!! 31 | # response = httpx.get( 32 | # "https://httpbin.org/get", 33 | # params={"foo": "bar", "spam": "eggs"}, 34 | # ) 35 | async with httpx.AsyncClient() as client: 36 | response = await client.get( 37 | "https://httpbin.org/get", 38 | params={ 39 | "foo": "bar", 40 | "spam": "eggs", 41 | "fizz": "buzz", 42 | }, 43 | ) 44 | log.info("fetched data from api %s", response.json()) 45 | await asyncio.sleep(5) 46 | subject = "Welcome to our Shop!" 47 | body = body_template.format( 48 | name=user.full_name or user.username, 49 | ) 50 | await send_email( 51 | recipient=str(user.email), 52 | subject=subject, 53 | body=body, 54 | ) 55 | log.info("Successfully sent welcome email to %s", user) 56 | 57 | 58 | if __name__ == "__main__": 59 | logging.basicConfig( 60 | level=logging.INFO, 61 | format=LOG_DEFAULT_FORMAT, 62 | ) 63 | # noinspection PyTypeChecker 64 | asyncio.run( 65 | send_welcome_email( 66 | UserBase( 67 | username="john", 68 | full_name="John Smith", 69 | email="john.smith@email.com", 70 | ) 71 | ) 72 | ) 73 | -------------------------------------------------------------------------------- /open-lessons/fastapi-bg-tasks-20.05.2025/api/api_v1/users/views.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from uuid import uuid4 3 | 4 | from fastapi import APIRouter, BackgroundTasks 5 | 6 | from api.api_v1.users.schemas import UserRead, UserCreate 7 | from api.api_v1.users.utils.welcome_email_sender import send_welcome_email 8 | 9 | log = logging.getLogger(__name__) 10 | 11 | 12 | router = APIRouter( 13 | prefix="/users", 14 | tags=["users"], 15 | ) 16 | 17 | 18 | @router.post("/register") 19 | async def register( 20 | user_create: UserCreate, 21 | background_tasks: BackgroundTasks, 22 | ) -> UserRead: 23 | log.info("Create new user %s", user_create) 24 | 25 | # мы будто бы сохранили юзера в базу 26 | user = UserRead( 27 | id=uuid4(), 28 | **user_create.model_dump(), 29 | ) 30 | log.info("User %s created", user) 31 | # await send_welcome_email(user) 32 | background_tasks.add_task(send_welcome_email, user=user) 33 | return user 34 | -------------------------------------------------------------------------------- /open-lessons/fastapi-bg-tasks-20.05.2025/app.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from fastapi import FastAPI, Request 4 | 5 | from api import router as api_router 6 | from config import LOG_DEFAULT_FORMAT 7 | 8 | logging.basicConfig( 9 | level=logging.INFO, 10 | format=LOG_DEFAULT_FORMAT, 11 | ) 12 | 13 | app = FastAPI() 14 | app.include_router(api_router) 15 | 16 | 17 | @app.get("/") 18 | def read_root(request: Request): 19 | url = request.url.replace(path="/docs") 20 | return { 21 | "docs": str(url), 22 | } 23 | -------------------------------------------------------------------------------- /open-lessons/fastapi-bg-tasks-20.05.2025/config.py: -------------------------------------------------------------------------------- 1 | LOG_DEFAULT_FORMAT = ( 2 | "[%(asctime)s] %(module)10s:%(lineno)-3d %(levelname)-7s - %(message)s" 3 | ) 4 | ADMIN_EMAIL = "admin@shop.com" 5 | -------------------------------------------------------------------------------- /open-lessons/fastapi-bg-tasks-20.05.2025/docker-compose.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | maildev: 3 | image: maildev/maildev 4 | environment: 5 | MAILDEV_SMTP_PORT: 1025 6 | MAILDEV_WEB_PORT: 1080 7 | TZ: Europe/Moscow 8 | ports: 9 | - "1080:1080" 10 | - "1025:1025" 11 | logging: 12 | driver: "json-file" 13 | options: 14 | max-size: "1m" 15 | -------------------------------------------------------------------------------- /open-lessons/fastapi-bg-tasks-20.05.2025/pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "fastapi-bg-tasks-20-05" 3 | version = "0.1.0" 4 | description = "Add your description here" 5 | readme = "README.md" 6 | requires-python = ">=3.13" 7 | dependencies = [ 8 | "aiosmtplib>=4.0.1", 9 | "fastapi[standard]>=0.115.12", 10 | ] 11 | 12 | [dependency-groups] 13 | dev = [ 14 | "black>=25.1.0", 15 | ] 16 | -------------------------------------------------------------------------------- /open-lessons/fastapi-intro-27.02.2025/main.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | 3 | from views import router as api_router 4 | 5 | app = FastAPI() 6 | app.include_router(api_router) 7 | 8 | 9 | @app.get( 10 | "/", 11 | summary="Hello World", 12 | ) 13 | def read_root_hello_world(): 14 | """ 15 | Greets the World! 16 | 17 | # Header 1 18 | ## Header 2 19 | ### Header 3 20 | 21 | ToDo List: 22 | - learn FastAPI 23 | - learn Django 24 | - learn SQLAlchemy 25 | 26 | Example: 27 | ```json 28 | {"message": "Hello World!"} 29 | ``` 30 | """ 31 | return {"message": "Hello World"} 32 | 33 | 34 | @app.get("/hello/") 35 | def greet_by_name(name: str): 36 | return {"message": f"Hello, {name}!"} 37 | -------------------------------------------------------------------------------- /open-lessons/fastapi-intro-27.02.2025/requirements.txt: -------------------------------------------------------------------------------- 1 | annotated-types==0.7.0 2 | anyio==4.8.0 3 | certifi==2025.1.31 4 | click==8.1.8 5 | dnspython==2.7.0 6 | email_validator==2.2.0 7 | fastapi==0.115.9 8 | fastapi-cli==0.0.7 9 | h11==0.14.0 10 | httpcore==1.0.7 11 | httptools==0.6.4 12 | httpx==0.28.1 13 | idna==3.10 14 | itsdangerous==2.2.0 15 | Jinja2==3.1.5 16 | markdown-it-py==3.0.0 17 | MarkupSafe==3.0.2 18 | mdurl==0.1.2 19 | orjson==3.10.15 20 | pydantic==2.10.6 21 | pydantic-extra-types==2.10.2 22 | pydantic-settings==2.8.1 23 | pydantic_core==2.27.2 24 | Pygments==2.19.1 25 | python-dotenv==1.0.1 26 | python-multipart==0.0.20 27 | PyYAML==6.0.2 28 | rich==13.9.4 29 | rich-toolkit==0.13.2 30 | shellingham==1.5.4 31 | sniffio==1.3.1 32 | starlette==0.45.3 33 | typer==0.15.1 34 | typing_extensions==4.12.2 35 | ujson==5.10.0 36 | uvicorn==0.34.0 37 | uvloop==0.21.0 38 | watchfiles==1.0.4 39 | websockets==15.0 40 | -------------------------------------------------------------------------------- /open-lessons/fastapi-intro-27.02.2025/views/__init__.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter 2 | 3 | from .movies.views import router as movies_router 4 | 5 | router = APIRouter( 6 | prefix="/api", 7 | tags=["API"], 8 | ) 9 | 10 | router.include_router(movies_router) 11 | -------------------------------------------------------------------------------- /open-lessons/fastapi-intro-27.02.2025/views/movies/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OtusTeam/BasePython/d87ee93755f4c2dbb4c2b4bbd35743a2c6e44043/open-lessons/fastapi-intro-27.02.2025/views/movies/__init__.py -------------------------------------------------------------------------------- /open-lessons/fastapi-intro-27.02.2025/views/movies/crud.py: -------------------------------------------------------------------------------- 1 | """ 2 | Create 3 | Read 4 | Update 5 | Delete 6 | """ 7 | from pydantic import BaseModel 8 | 9 | from .schema import Movie, MovieCreate 10 | 11 | 12 | class MoviesStorage(BaseModel): 13 | last_id: int = 0 14 | movies: dict[int, Movie] = {} 15 | 16 | @property 17 | def next_id(self) -> int: 18 | self.last_id += 1 19 | return self.last_id 20 | 21 | def get(self) -> list[Movie]: 22 | return list(self.movies.values()) 23 | 24 | def get_by_id(self, movie_id: int) -> Movie | None: 25 | return self.movies.get(movie_id) 26 | 27 | def add(self, movie_in: MovieCreate) -> Movie: 28 | movie = Movie( 29 | id=self.next_id, 30 | **movie_in.model_dump(), 31 | ) 32 | self.movies[movie.id] = movie 33 | return movie 34 | 35 | def delete(self, movie_id: int) -> None: 36 | self.movies.pop(movie_id, None) 37 | 38 | 39 | storage = MoviesStorage() 40 | 41 | storage.add( 42 | MovieCreate( 43 | title="The Shawshank Redemption", 44 | year=1994, 45 | ), 46 | ) 47 | storage.add( 48 | MovieCreate( 49 | title="Pulp Fiction", 50 | year=1994, 51 | ), 52 | ) 53 | storage.add( 54 | MovieCreate( 55 | title="The Game", 56 | year=1997, 57 | ), 58 | ) -------------------------------------------------------------------------------- /open-lessons/fastapi-intro-27.02.2025/views/movies/schema.py: -------------------------------------------------------------------------------- 1 | from typing import Annotated 2 | 3 | from annotated_types import Len, Ge, Le 4 | from pydantic import BaseModel, Field 5 | 6 | 7 | 8 | class MovieBase(BaseModel): 9 | title: str 10 | year: int 11 | 12 | 13 | class MovieCreate(MovieBase): 14 | title: Annotated[ 15 | str, 16 | Len(min_length=1, max_length=200), 17 | ] 18 | year: Annotated[ 19 | int, 20 | Ge(1878), 21 | Le(2222), 22 | ] 23 | 24 | 25 | class MovieRead(MovieBase): 26 | id: int = Field(example=42) 27 | 28 | 29 | class Movie(MovieBase): 30 | """ 31 | The Movie model 32 | """ 33 | id: int 34 | -------------------------------------------------------------------------------- /open-lessons/fastapi-intro-27.02.2025/views/movies/views.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter, HTTPException, status 2 | from pydantic import PositiveInt 3 | 4 | from .crud import storage 5 | from .schema import MovieRead, MovieCreate 6 | 7 | router = APIRouter( 8 | prefix="/movies", 9 | tags=["Movies"], 10 | ) 11 | 12 | @router.get( 13 | "/", 14 | response_model=list[MovieRead], 15 | ) 16 | def get_movies(): 17 | return storage.get() 18 | 19 | 20 | @router.post( 21 | "/", 22 | response_model=MovieRead, 23 | ) 24 | def create_movie( 25 | movie_in: MovieCreate, 26 | ): 27 | return storage.add(movie_in=movie_in) 28 | 29 | 30 | @router.get( 31 | "/{movie_id}/", 32 | response_model=MovieRead, 33 | ) 34 | def get_movie( 35 | movie_id: PositiveInt, 36 | ): 37 | movie = storage.get_by_id(movie_id=movie_id) 38 | if movie: 39 | return movie 40 | raise HTTPException( 41 | status_code=status.HTTP_404_NOT_FOUND, 42 | detail=f"Movie #{movie_id} not found!", 43 | ) 44 | 45 | @router.delete( 46 | "/{movie_id}/", 47 | status_code=status.HTTP_204_NO_CONTENT, 48 | ) 49 | def get_movie( 50 | movie_id: PositiveInt, 51 | ) -> None: 52 | storage.delete(movie_id=movie_id) 53 | -------------------------------------------------------------------------------- /open-lessons/fastapi-jsonapi.03.06.2024/config.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | CURRENT_FILE = Path(__file__).resolve() 4 | PROJECT_DIR = CURRENT_FILE.parent 5 | DB_URL = f"sqlite+aiosqlite:///{PROJECT_DIR}/db.sqlite3" 6 | # DB_ECHO = False 7 | DB_ECHO = True 8 | -------------------------------------------------------------------------------- /open-lessons/fastapi-jsonapi.03.06.2024/db.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.ext.asyncio import async_sessionmaker 2 | 3 | from models.database import create_engine 4 | 5 | 6 | def async_session() -> async_sessionmaker: 7 | engine = create_engine() 8 | _async_session = async_sessionmaker(bind=engine, expire_on_commit=False) 9 | return _async_session 10 | 11 | 12 | class Connector: 13 | @classmethod 14 | async def get_session(cls): 15 | """ 16 | Get session as dependency 17 | """ 18 | sess = async_session() 19 | async with sess() as db_session: 20 | yield db_session 21 | await db_session.rollback() 22 | -------------------------------------------------------------------------------- /open-lessons/fastapi-jsonapi.03.06.2024/main.py: -------------------------------------------------------------------------------- 1 | from contextlib import asynccontextmanager 2 | from pathlib import Path 3 | 4 | import uvicorn 5 | from fastapi import ( 6 | FastAPI, 7 | APIRouter, 8 | ) 9 | 10 | from models.database import sqlalchemy_init 11 | from urls import register_routes 12 | 13 | CURRENT_FILE = Path(__file__).resolve() 14 | PROJECT_DIR = CURRENT_FILE.parent 15 | DB_URL = f"sqlite+aiosqlite:///{PROJECT_DIR}/db.sqlite3" 16 | 17 | 18 | @asynccontextmanager 19 | async def lifespan(app: FastAPI): 20 | await sqlalchemy_init() 21 | yield 22 | 23 | 24 | router = APIRouter(prefix="/api") 25 | register_routes(router) 26 | 27 | app = FastAPI(lifespan=lifespan) 28 | app.include_router(router) 29 | 30 | if __name__ == "__main__": 31 | uvicorn.run( 32 | "main:app", 33 | host="0.0.0.0", 34 | port=8000, 35 | reload=True, 36 | ) 37 | -------------------------------------------------------------------------------- /open-lessons/fastapi-jsonapi.03.06.2024/models/__init__.py: -------------------------------------------------------------------------------- 1 | __all__ = ( 2 | "User", 3 | "UserBio", 4 | ) 5 | 6 | from .user import User 7 | from .user_bio import UserBio 8 | -------------------------------------------------------------------------------- /open-lessons/fastapi-jsonapi.03.06.2024/models/database.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import ( 2 | make_url, 3 | Integer, 4 | Column, 5 | ) 6 | from sqlalchemy.ext.asyncio import create_async_engine 7 | from sqlalchemy.orm import DeclarativeBase 8 | 9 | from config import ( 10 | DB_URL, 11 | DB_ECHO, 12 | ) 13 | 14 | 15 | class Base(DeclarativeBase): 16 | id = Column(Integer, primary_key=True) 17 | 18 | 19 | def create_engine(): 20 | return create_async_engine( 21 | url=make_url(DB_URL), 22 | echo=DB_ECHO, 23 | ) 24 | 25 | 26 | async def sqlalchemy_init() -> None: 27 | engine = create_engine() 28 | async with engine.begin() as conn: 29 | await conn.run_sync(Base.metadata.create_all) 30 | -------------------------------------------------------------------------------- /open-lessons/fastapi-jsonapi.03.06.2024/models/user.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import ( 2 | Column, 3 | Integer, 4 | String, 5 | ) 6 | from sqlalchemy.orm import relationship 7 | 8 | from .database import Base 9 | 10 | 11 | class User(Base): 12 | __tablename__ = "users" 13 | 14 | first_name = Column(String, nullable=False) 15 | last_name = Column(String, nullable=False) 16 | age = Column(Integer, nullable=True) 17 | email = Column(String, nullable=True) 18 | 19 | bio = relationship( 20 | "UserBio", 21 | back_populates="user", 22 | uselist=False, 23 | ) 24 | -------------------------------------------------------------------------------- /open-lessons/fastapi-jsonapi.03.06.2024/models/user_bio.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import ( 2 | Column, 3 | ForeignKey, 4 | Integer, 5 | String, 6 | ) 7 | from sqlalchemy.orm import relationship 8 | 9 | from .database import Base 10 | 11 | 12 | class UserBio(Base): 13 | __tablename__ = "user_bio" 14 | birth_city = Column(String, nullable=False, default="", server_default="") 15 | favourite_movies = Column(String, nullable=False, default="", server_default="") 16 | 17 | user_id = Column(Integer, ForeignKey("users.id"), nullable=False, unique=True) 18 | user = relationship( 19 | "User", 20 | back_populates="bio", 21 | uselist=False, 22 | ) 23 | 24 | def __repr__(self): 25 | return ( 26 | f"{self.__class__.__name__}(" 27 | f"id={self.id}," 28 | f" birth_city={self.birth_city!r}," 29 | f" favourite_movies={self.favourite_movies!r}," 30 | f" user_id={self.user_id}" 31 | ")" 32 | ) 33 | -------------------------------------------------------------------------------- /open-lessons/fastapi-jsonapi.03.06.2024/requirements.txt: -------------------------------------------------------------------------------- 1 | aiosqlite==0.20.0 2 | annotated-types==0.7.0 3 | anyio==4.4.0 4 | certifi==2024.6.2 5 | click==8.1.7 6 | dnspython==2.6.1 7 | email_validator==2.1.1 8 | fastapi==0.111.0 9 | fastapi-cli==0.0.4 10 | FastAPI-JSONAPI==2.8.0 11 | greenlet==3.0.3 12 | h11==0.14.0 13 | httpcore==1.0.5 14 | httptools==0.6.1 15 | httpx==0.27.0 16 | idna==3.7 17 | Jinja2==3.1.4 18 | markdown-it-py==3.0.0 19 | MarkupSafe==2.1.5 20 | mdurl==0.1.2 21 | orjson==3.10.3 22 | pydantic==1.10.15 23 | pydantic_core==2.18.3 24 | Pygments==2.18.0 25 | python-dotenv==1.0.1 26 | python-multipart==0.0.9 27 | PyYAML==6.0.1 28 | rich==13.7.1 29 | shellingham==1.5.4 30 | simplejson==3.19.2 31 | sniffio==1.3.1 32 | SQLAlchemy==2.0.30 33 | starlette==0.37.2 34 | typer==0.12.3 35 | typing_extensions==4.12.1 36 | ujson==5.10.0 37 | uvicorn==0.30.1 38 | uvloop==0.19.0 39 | watchfiles==0.22.0 40 | websockets==12.0 41 | -------------------------------------------------------------------------------- /open-lessons/fastapi-jsonapi.03.06.2024/schemas/__init__.py: -------------------------------------------------------------------------------- 1 | __all__ = ( 2 | "UserSchema", 3 | "UserUpdateSchema", 4 | "UserBioSchema", 5 | "UserBioUpdateSchema", 6 | ) 7 | 8 | from .user import UserSchema, UserUpdateSchema 9 | from .user_bio import UserBioSchema, UserBioUpdateSchema 10 | -------------------------------------------------------------------------------- /open-lessons/fastapi-jsonapi.03.06.2024/schemas/user.py: -------------------------------------------------------------------------------- 1 | 2 | from datetime import datetime 3 | from typing import TYPE_CHECKING, List, Optional 4 | 5 | from fastapi_jsonapi.schema_base import BaseModel, Field, RelationshipInfo 6 | 7 | if TYPE_CHECKING: 8 | # from .post import PostSchema 9 | from .user_bio import UserBioSchema 10 | 11 | 12 | class UserBaseSchema(BaseModel): 13 | """User base schema.""" 14 | 15 | class Config: 16 | """Pydantic schema config.""" 17 | 18 | orm_mode = True 19 | 20 | first_name: str 21 | last_name: str 22 | age: int | None = None 23 | email: str | None = None 24 | 25 | bio: Optional["UserBioSchema"] = Field( 26 | relationship=RelationshipInfo( 27 | resource_type="user_bio", 28 | ), 29 | ) 30 | 31 | 32 | class UserUpdateSchema(UserBaseSchema): 33 | """User PATCH schema.""" 34 | 35 | first_name: str | None = None 36 | last_name: str | None = None 37 | 38 | 39 | # class UserCreateSchema(UserBaseSchema): 40 | # """User input schema.""" 41 | 42 | 43 | class UserSchema(UserBaseSchema): 44 | """User item schema.""" 45 | 46 | id: int 47 | -------------------------------------------------------------------------------- /open-lessons/fastapi-jsonapi.03.06.2024/schemas/user_bio.py: -------------------------------------------------------------------------------- 1 | from typing import TYPE_CHECKING 2 | 3 | from fastapi_jsonapi.schema_base import BaseModel, Field, RelationshipInfo 4 | 5 | if TYPE_CHECKING: 6 | from .user import UserSchema 7 | 8 | 9 | class UserBioBaseSchema(BaseModel): 10 | """UserBio base schema.""" 11 | 12 | class Config: 13 | """Pydantic schema config.""" 14 | 15 | orm_mode = True 16 | 17 | birth_city: str 18 | favourite_movies: str 19 | 20 | user: "UserSchema" = Field( 21 | relationship=RelationshipInfo( 22 | resource_type="user", 23 | ), 24 | ) 25 | 26 | 27 | class UserBioUpdateSchema(UserBioBaseSchema): 28 | """UserBio PATCH schema.""" 29 | 30 | birth_city: str | None = None 31 | favourite_movies: str | None = None 32 | 33 | # class UserBioInSchema(UserBioBaseSchema): 34 | # """UserBio input schema.""" 35 | 36 | 37 | class UserBioSchema(UserBioBaseSchema): 38 | """UserBio item schema.""" 39 | 40 | class Config: 41 | """Pydantic model config.""" 42 | 43 | orm_mode = True 44 | 45 | id: int 46 | -------------------------------------------------------------------------------- /open-lessons/fastapi-jsonapi.03.06.2024/urls.py: -------------------------------------------------------------------------------- 1 | from typing import ClassVar 2 | 3 | from fastapi import ( 4 | APIRouter, 5 | Depends, 6 | ) 7 | 8 | from fastapi_jsonapi import RoutersJSONAPI 9 | from fastapi_jsonapi.misc.sqla.generics.base import ( 10 | ListViewBaseGeneric, 11 | DetailViewBaseGeneric, 12 | ) 13 | from fastapi_jsonapi.views.utils import ( 14 | HTTPMethodConfig, 15 | HTTPMethod, 16 | ) 17 | from fastapi_jsonapi.views.view_base import ViewBase 18 | from pydantic import BaseModel 19 | from sqlalchemy.ext.asyncio import AsyncSession 20 | 21 | from db import Connector 22 | from models import ( 23 | User, 24 | UserBio, 25 | ) 26 | from schemas import ( 27 | UserSchema, 28 | UserUpdateSchema, 29 | UserBioSchema, 30 | UserBioUpdateSchema, 31 | ) 32 | 33 | 34 | class SessionDependency(BaseModel): 35 | session: AsyncSession = Depends(Connector.get_session) 36 | 37 | class Config: 38 | arbitrary_types_allowed = True 39 | 40 | 41 | def session_dependency_handler(view: ViewBase, dto: SessionDependency): 42 | return { 43 | "session": dto.session, 44 | } 45 | 46 | 47 | class DetailView(DetailViewBaseGeneric): 48 | method_dependencies: ClassVar = { 49 | HTTPMethod.ALL: HTTPMethodConfig( 50 | dependencies=SessionDependency, 51 | prepare_data_layer_kwargs=session_dependency_handler, 52 | ), 53 | } 54 | 55 | 56 | class ListView(ListViewBaseGeneric): 57 | method_dependencies: ClassVar = { 58 | HTTPMethod.ALL: HTTPMethodConfig( 59 | dependencies=SessionDependency, 60 | prepare_data_layer_kwargs=session_dependency_handler, 61 | ), 62 | } 63 | 64 | 65 | def register_routes(router: APIRouter) -> None: 66 | default_methods = [ 67 | RoutersJSONAPI.Methods.GET_LIST, 68 | RoutersJSONAPI.Methods.POST, 69 | RoutersJSONAPI.Methods.GET, 70 | RoutersJSONAPI.Methods.PATCH, 71 | RoutersJSONAPI.Methods.DELETE, 72 | ] 73 | RoutersJSONAPI( 74 | router=router, 75 | path="/users", 76 | tags=["User"], 77 | class_detail=DetailView, 78 | class_list=ListView, 79 | model=User, 80 | schema=UserSchema, 81 | resource_type="user", 82 | schema_in_patch=UserUpdateSchema, 83 | methods=default_methods, 84 | ) 85 | 86 | RoutersJSONAPI( 87 | router=router, 88 | path="/user-bio", 89 | tags=["User", "User Bio"], 90 | class_detail=DetailView, 91 | class_list=ListView, 92 | model=UserBio, 93 | schema=UserBioSchema, 94 | resource_type="user_bio", 95 | schema_in_patch=UserBioUpdateSchema, 96 | methods=default_methods, 97 | ) 98 | -------------------------------------------------------------------------------- /open-lessons/fastapi.24.10.2023/main.py: -------------------------------------------------------------------------------- 1 | import uvicorn 2 | from fastapi import FastAPI 3 | 4 | from views.calc import router as calc_router 5 | from views.items import router as items_router 6 | 7 | app = FastAPI() 8 | app.include_router(calc_router) 9 | app.include_router(items_router) 10 | 11 | 12 | @app.get("/") 13 | def hello_index(): 14 | return {"message": "Hello Index!"} 15 | 16 | 17 | @app.get("/hello/") 18 | def hello_world(name: str = "World"): 19 | return {"message": f"Hello {name}!"} 20 | 21 | 22 | @app.get("/hello/{name}/") 23 | def hello_view(name: str): 24 | return {"message": f"Hello {name.title()}!"} 25 | 26 | 27 | if __name__ == "__main__": 28 | uvicorn.run( 29 | "main:app", 30 | # app=app, 31 | reload=True, 32 | ) 33 | # uvicorn.run(app) 34 | -------------------------------------------------------------------------------- /open-lessons/fastapi.24.10.2023/requirements.txt: -------------------------------------------------------------------------------- 1 | annotated-types==0.6.0 2 | anyio==3.7.1 3 | certifi==2023.7.22 4 | click==8.1.7 5 | dnspython==2.4.2 6 | email-validator==2.1.0.post1 7 | fastapi==0.104.0 8 | h11==0.14.0 9 | httpcore==0.18.0 10 | httptools==0.6.1 11 | httpx==0.25.0 12 | idna==3.4 13 | itsdangerous==2.1.2 14 | Jinja2==3.1.2 15 | MarkupSafe==2.1.3 16 | orjson==3.9.9 17 | pydantic==2.4.2 18 | pydantic-extra-types==2.1.0 19 | pydantic-settings==2.0.3 20 | pydantic_core==2.10.1 21 | python-dotenv==1.0.0 22 | python-multipart==0.0.6 23 | PyYAML==6.0.1 24 | sniffio==1.3.0 25 | starlette==0.27.0 26 | typing_extensions==4.8.0 27 | ujson==5.8.0 28 | uvicorn==0.23.2 29 | uvloop==0.19.0 30 | watchfiles==0.21.0 31 | websockets==12.0 32 | -------------------------------------------------------------------------------- /open-lessons/fastapi.24.10.2023/views/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OtusTeam/BasePython/d87ee93755f4c2dbb4c2b4bbd35743a2c6e44043/open-lessons/fastapi.24.10.2023/views/__init__.py -------------------------------------------------------------------------------- /open-lessons/fastapi.24.10.2023/views/calc/__init__.py: -------------------------------------------------------------------------------- 1 | from .views import router 2 | -------------------------------------------------------------------------------- /open-lessons/fastapi.24.10.2023/views/calc/crud.py: -------------------------------------------------------------------------------- 1 | from .schemas import CalcInput 2 | 3 | 4 | def calc_mul(calc: CalcInput): 5 | return { 6 | "total": calc.a * calc.b, 7 | "calc": calc.model_dump(), 8 | } 9 | -------------------------------------------------------------------------------- /open-lessons/fastapi.24.10.2023/views/calc/schemas.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel 2 | 3 | 4 | class CalcInput(BaseModel): 5 | a: int 6 | b: int 7 | -------------------------------------------------------------------------------- /open-lessons/fastapi.24.10.2023/views/calc/views.py: -------------------------------------------------------------------------------- 1 | from fastapi import Depends, APIRouter 2 | 3 | from . import crud 4 | from .schemas import CalcInput 5 | 6 | router = APIRouter(prefix="/calc", tags=["Calc"]) 7 | 8 | 9 | @router.get("/add/") 10 | def calc_add(a: int, b: int): 11 | return { 12 | "total": a + b, 13 | "a": a, 14 | "b": b, 15 | } 16 | 17 | 18 | @router.get("/mul/") 19 | def calc_mul(calc: CalcInput = Depends()): 20 | return crud.calc_mul(calc=calc) 21 | -------------------------------------------------------------------------------- /open-lessons/fastapi.24.10.2023/views/items/__init__.py: -------------------------------------------------------------------------------- 1 | from .views import router 2 | -------------------------------------------------------------------------------- /open-lessons/fastapi.24.10.2023/views/items/schemas.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel, Field, EmailStr 2 | 3 | 4 | class NewItemModel(BaseModel): 5 | # username: constr(min_length=3) 6 | username: str = Field(min_length=3) 7 | email: EmailStr 8 | -------------------------------------------------------------------------------- /open-lessons/fastapi.24.10.2023/views/items/views.py: -------------------------------------------------------------------------------- 1 | from typing import Annotated 2 | from fastapi import APIRouter, Path 3 | 4 | from .schemas import NewItemModel 5 | 6 | router = APIRouter(prefix="/items", tags=["Items"]) 7 | 8 | 9 | @router.post("/") 10 | def create_item(item: NewItemModel): 11 | return {"item": item.model_dump()} 12 | 13 | 14 | @router.get("/{product_id}/") 15 | def get_item( 16 | product_id: Annotated[int, Path(gt=0, lt=1_000_000)], 17 | ): 18 | return { 19 | "item": 20 | { 21 | "id": product_id, 22 | "name": f"product_{product_id}", 23 | }, 24 | } 25 | -------------------------------------------------------------------------------- /open-lessons/flask-.21.02.2023/main.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, request, render_template 2 | 3 | from views.products import products_app 4 | 5 | app = Flask(__name__) 6 | app.register_blueprint(products_app) 7 | 8 | 9 | @app.route("/") 10 | def index_view(): 11 | return render_template("index.html") 12 | 13 | 14 | @app.route("/hello/") 15 | @app.route("/hello//") 16 | def get_hello(name=None): 17 | if name is None: 18 | name = request.args.get("name") or "" 19 | if not name: 20 | name = "World" 21 | # return f"

Hello {name}!

" 22 | return render_template("hello.html", name=name) 23 | 24 | 25 | @app.route("/items//") 26 | def get_item_by_id(item_id): 27 | return { 28 | "data": { 29 | "id": item_id, 30 | "name": f"Name_{item_id}" 31 | } 32 | } 33 | 34 | 35 | if __name__ == "__main__": 36 | app.run(debug=True) 37 | -------------------------------------------------------------------------------- /open-lessons/flask-.21.02.2023/requirements.txt: -------------------------------------------------------------------------------- 1 | click==8.1.3 2 | Flask==2.2.3 3 | itsdangerous==2.1.2 4 | Jinja2==3.1.2 5 | MarkupSafe==2.1.2 6 | Werkzeug==2.2.3 7 | -------------------------------------------------------------------------------- /open-lessons/flask-.21.02.2023/templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 8 | {% block title %}Base Title{% endblock %} 9 | 10 | 11 | {% block body %} 12 | 13 | {% endblock %} 14 |
15 |
16 | © OTUS 2023 17 |
18 | 19 | 20 | -------------------------------------------------------------------------------- /open-lessons/flask-.21.02.2023/templates/hello.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block title %} 4 | Hello page 5 | {% endblock %} 6 | 7 | {% block body %} 8 |

Hello {{ name }}

9 |

Time 0

10 |
11 |

12 |   
13 | 27 | {% endblock %} 28 | -------------------------------------------------------------------------------- /open-lessons/flask-.21.02.2023/templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block title %} 4 | Index page 5 | {% endblock %} 6 | 7 | {% block body %} 8 |

Index Page!!!

9 | {% endblock %} 10 | -------------------------------------------------------------------------------- /open-lessons/flask-.21.02.2023/templates/products/create.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block title %} 4 | Create Product 5 | {% endblock %} 6 | 7 | {% block body %} 8 |

Create a new product

9 | 10 |
11 |
12 | 13 | 14 | 15 |
16 |
17 | 18 |
19 | 20 | Back to products list 21 | 22 |
23 | {% endblock %} 24 | -------------------------------------------------------------------------------- /open-lessons/flask-.21.02.2023/templates/products/details.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block title %} 4 | Product #{{ product.id }} details 5 | {% endblock %} 6 | 7 | {% block body %} 8 |

Product {{ product.name }}

9 | 10 |
11 | 12 | Back to products list 13 | 14 |
15 | {% endblock %} 16 | -------------------------------------------------------------------------------- /open-lessons/flask-.21.02.2023/templates/products/list.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block title %} 4 | Products 5 | {% endblock %} 6 | 7 | {% block body %} 8 |

Products list:

9 | 10 |
11 | 20 |
21 | 22 |
23 | Create a new product 24 |
25 | {% endblock %} -------------------------------------------------------------------------------- /open-lessons/flask-.21.02.2023/views/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OtusTeam/BasePython/d87ee93755f4c2dbb4c2b4bbd35743a2c6e44043/open-lessons/flask-.21.02.2023/views/__init__.py -------------------------------------------------------------------------------- /open-lessons/flask-.21.02.2023/views/products.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | 3 | from flask import Blueprint, render_template, request, redirect, url_for 4 | from werkzeug.exceptions import BadRequest, NotFound 5 | 6 | 7 | products_app = Blueprint( 8 | "products_app", 9 | __name__, 10 | url_prefix="/products", 11 | ) 12 | 13 | 14 | @dataclass(frozen=True, slots=True) 15 | class Product: 16 | id: int 17 | name: str 18 | 19 | 20 | @dataclass(slots=True) 21 | class ProductsStorage: 22 | products: dict[int, Product] 23 | last_index: int = 0 24 | 25 | @property 26 | def next_index(self): 27 | self.last_index += 1 28 | return self.last_index 29 | 30 | def create(self, name: str): 31 | product = Product(id=self.next_index, name=name) 32 | self.products[product.id] = product 33 | return product 34 | 35 | 36 | storage = ProductsStorage(products={}) 37 | storage.create("Laptop") 38 | storage.create("Desktop") 39 | storage.create("Smartphone") 40 | 41 | 42 | @products_app.route("/", endpoint="list") 43 | def get_products_list(): 44 | products = list(storage.products.values()) 45 | return render_template("products/list.html", products=products) 46 | 47 | 48 | @products_app.route("//", endpoint="details") 49 | def get_product_by_id(product_id): 50 | product = storage.products.get(product_id) 51 | if product is None: 52 | raise NotFound(f"Product #{product_id} not found!") 53 | return render_template("products/details.html", product=product) 54 | 55 | 56 | @products_app.route("/create/", methods=["GET", "POST"], endpoint="create") 57 | def create_product(): 58 | if request.method == "GET": 59 | return render_template("products/create.html") 60 | 61 | product_name = request.form.get("product-name") or "" 62 | product_name = product_name.strip() 63 | if not product_name: 64 | raise BadRequest("Field `product-name` is required!") 65 | 66 | product = storage.create(name=product_name) 67 | url = url_for('products_app.details', product_id=product.id) 68 | return redirect(url) 69 | -------------------------------------------------------------------------------- /open-lessons/flask-.21.02.2023/wsgi.py: -------------------------------------------------------------------------------- 1 | __all__ = ("app",) 2 | 3 | from main import app 4 | -------------------------------------------------------------------------------- /open-lessons/flask-jsonapi.19.08.2021/app.py: -------------------------------------------------------------------------------- 1 | from flask import Flask 2 | from flask_combo_jsonapi import Api 3 | from combojsonapi.spec import ApiSpecPlugin 4 | 5 | from models.database import db 6 | from views import PersonList, PersonDetail, ComputerList, ComputerDetail 7 | 8 | app = Flask(__name__) 9 | 10 | app.config.from_object("config.DevelopmentConfig") 11 | db.init_app(app) 12 | 13 | 14 | api_spec_plugin = ApiSpecPlugin( 15 | app=app, 16 | tags={ 17 | "Person": "Persons API", 18 | "Computer": "Computers API", 19 | }, 20 | ) 21 | 22 | api = Api( 23 | app, 24 | plugins=[ 25 | api_spec_plugin, 26 | ], 27 | ) 28 | 29 | api.route(PersonList, "person_list", "/persons", tag="Person") 30 | api.route(PersonDetail, "person_detail", "/persons/", tag="Person") 31 | api.route(ComputerList, "computer_list", "/computers", tag="Computer") 32 | api.route(ComputerDetail, "computer_detail", "/computers/", tag="Computer") 33 | 34 | 35 | @app.route("/") 36 | def hello(): 37 | db.create_all() # use for only for dev 38 | return {"message": "Hello world!"} 39 | 40 | 41 | if __name__ == "__main__": 42 | app.run() 43 | -------------------------------------------------------------------------------- /open-lessons/flask-jsonapi.19.08.2021/config.py: -------------------------------------------------------------------------------- 1 | class BaseConfig: 2 | SQLALCHEMY_DATABASE_URI = "sqlite:////:memory:" 3 | SQLALCHEMY_ECHO = False 4 | SQLALCHEMY_TRACK_MODIFICATIONS = False 5 | 6 | # swagger 7 | 8 | OPENAPI_URL_PREFIX = "/docs" 9 | OPENAPI_VERSION = "2.0.0" 10 | OPENAPI_SWAGGER_UI_PATH = "/" 11 | OPENAPI_SWAGGER_UI_VERSION = "3.45.0" 12 | 13 | 14 | class DevelopmentConfig(BaseConfig): 15 | DEBUG = True 16 | SQLALCHEMY_DATABASE_URI = "sqlite:///./flask-jsonapi.db" 17 | -------------------------------------------------------------------------------- /open-lessons/flask-jsonapi.19.08.2021/models/__init__.py: -------------------------------------------------------------------------------- 1 | from .person import Person 2 | from .computer import Computer 3 | 4 | __all__ = ( 5 | "Person", 6 | "Computer", 7 | ) 8 | -------------------------------------------------------------------------------- /open-lessons/flask-jsonapi.19.08.2021/models/computer.py: -------------------------------------------------------------------------------- 1 | from models.database import db 2 | 3 | 4 | class Computer(db.Model): 5 | id = db.Column(db.Integer, primary_key=True) 6 | serial = db.Column(db.String) 7 | person_id = db.Column(db.Integer, db.ForeignKey("person.id")) 8 | person = db.relationship("Person", back_populates="computers") 9 | -------------------------------------------------------------------------------- /open-lessons/flask-jsonapi.19.08.2021/models/database.py: -------------------------------------------------------------------------------- 1 | from flask_sqlalchemy import SQLAlchemy 2 | 3 | db = SQLAlchemy() 4 | -------------------------------------------------------------------------------- /open-lessons/flask-jsonapi.19.08.2021/models/person.py: -------------------------------------------------------------------------------- 1 | from .database import db 2 | 3 | 4 | class Person(db.Model): 5 | id = db.Column(db.Integer, primary_key=True) 6 | name = db.Column(db.String) 7 | birth_date = db.Column(db.Date) 8 | computers = db.relationship("Computer", back_populates="person") 9 | -------------------------------------------------------------------------------- /open-lessons/flask-jsonapi.19.08.2021/requirements.txt: -------------------------------------------------------------------------------- 1 | apispec==4.7.1 2 | click==8.0.1 3 | ComboJSONAPI==1.1.1 4 | Flask==1.1.2 5 | Flask-COMBO-JSONAPI==1.0.7 6 | Flask-SQLAlchemy==2.5.1 7 | itsdangerous==2.0.1 8 | Jinja2==3.0.1 9 | MarkupSafe==2.0.1 10 | marshmallow==3.2.1 11 | marshmallow-jsonapi==0.24.0 12 | PyYAML==5.4.1 13 | simplejson==3.17.3 14 | SQLAlchemy==1.3.24 15 | Werkzeug==2.0.1 16 | -------------------------------------------------------------------------------- /open-lessons/flask-jsonapi.19.08.2021/schemas/__init__.py: -------------------------------------------------------------------------------- 1 | from .person import PersonSchema 2 | from .computer import ComputerSchema 3 | 4 | __all__ = ( 5 | "PersonSchema", 6 | "ComputerSchema", 7 | ) 8 | -------------------------------------------------------------------------------- /open-lessons/flask-jsonapi.19.08.2021/schemas/computer.py: -------------------------------------------------------------------------------- 1 | from marshmallow_jsonapi import fields 2 | from marshmallow_jsonapi.flask import Schema 3 | 4 | from combojsonapi.utils import Relationship 5 | 6 | 7 | class ComputerSchema(Schema): 8 | class Meta: 9 | type_ = "computer" 10 | self_view = "computer_detail" 11 | self_view_kwargs = {"id": ""} 12 | self_view_many = "computer_list" 13 | 14 | id = fields.Integer(as_string=True, dump_only=True) 15 | serial = fields.String(required=True) 16 | owner = Relationship( 17 | type_="person", 18 | attribute="person", 19 | schema="PersonSchema", 20 | nested="PersonSchema", 21 | related_view="person_detail", 22 | related_view_kwargs={"id": ""}, 23 | self_view="computer_person", 24 | self_view_kwargs={"id": ""}, 25 | ) 26 | -------------------------------------------------------------------------------- /open-lessons/flask-jsonapi.19.08.2021/schemas/person.py: -------------------------------------------------------------------------------- 1 | from marshmallow_jsonapi import fields 2 | from marshmallow_jsonapi.flask import Schema 3 | from combojsonapi.utils import Relationship 4 | 5 | 6 | class PersonSchema(Schema): 7 | class Meta: 8 | type_ = "person" 9 | self_view_many = "person_list" 10 | self_view = "person_detail" 11 | self_view_kwargs = {"id": ""} 12 | 13 | id = fields.Integer(as_string=True) 14 | name = fields.String() 15 | birth_date = fields.Date() 16 | computers = Relationship( 17 | type_="computer", 18 | schema="ComputerSchema", 19 | nested="ComputerSchema", 20 | many=True, 21 | related_view="computer_list", 22 | # related_view_kwargs={"id": ""}, 23 | self_view="person_computers", 24 | self_view_kwargs={"id": ""}, 25 | ) 26 | -------------------------------------------------------------------------------- /open-lessons/flask-jsonapi.19.08.2021/views/__init__.py: -------------------------------------------------------------------------------- 1 | from .person import PersonList, PersonDetail 2 | from .computer import ComputerList, ComputerDetail 3 | 4 | __all__ = ( 5 | "PersonList", 6 | "PersonDetail", 7 | "ComputerList", 8 | "ComputerDetail", 9 | ) 10 | -------------------------------------------------------------------------------- /open-lessons/flask-jsonapi.19.08.2021/views/computer.py: -------------------------------------------------------------------------------- 1 | from flask_combo_jsonapi import ResourceList, ResourceDetail 2 | 3 | 4 | from models import Computer 5 | from models.database import db 6 | from schemas import ComputerSchema 7 | 8 | 9 | class ComputerList(ResourceList): 10 | schema = ComputerSchema 11 | data_layer = { 12 | "session": db.session, 13 | "model": Computer, 14 | } 15 | 16 | 17 | class ComputerDetail(ResourceDetail): 18 | schema = ComputerSchema 19 | data_layer = { 20 | "session": db.session, 21 | "model": Computer, 22 | } 23 | -------------------------------------------------------------------------------- /open-lessons/flask-jsonapi.19.08.2021/views/person.py: -------------------------------------------------------------------------------- 1 | from flask_combo_jsonapi import ResourceList, ResourceDetail 2 | 3 | from models import Person 4 | from models.database import db 5 | from schemas import PersonSchema 6 | 7 | 8 | class PersonList(ResourceList): 9 | schema = PersonSchema 10 | data_layer = { 11 | "session": db.session, 12 | "model": Person, 13 | } 14 | 15 | 16 | class PersonDetail(ResourceDetail): 17 | schema = PersonSchema 18 | data_layer = { 19 | "session": db.session, 20 | "model": Person, 21 | } 22 | -------------------------------------------------------------------------------- /open-lessons/flask-jsonapi.20.08.2021/app.py: -------------------------------------------------------------------------------- 1 | from flask import Flask 2 | from flask_combo_jsonapi import Api 3 | from combojsonapi.event import EventPlugin 4 | from combojsonapi.spec import ApiSpecPlugin 5 | from combojsonapi.permission import PermissionPlugin 6 | 7 | from models.database import db 8 | from views import PersonList, PersonDetail, ComputerList, ComputerDetail, PersonRelationship 9 | 10 | app = Flask(__name__) 11 | 12 | app.config.from_object("config.DevelopmentConfig") 13 | db.init_app(app) 14 | 15 | 16 | api_spec_plugin = ApiSpecPlugin( 17 | app=app, 18 | tags={ 19 | "Person": "Persons API", 20 | "Computer": "Computers API", 21 | }, 22 | ) 23 | 24 | 25 | event_plugin = EventPlugin(trailing_slash=False) 26 | 27 | permission_plugin = PermissionPlugin(strict=False) 28 | 29 | api = Api( 30 | app, 31 | plugins=[ 32 | event_plugin, 33 | api_spec_plugin, 34 | # permission_plugin, 35 | ], 36 | ) 37 | 38 | api.route( 39 | PersonList, "person_list", "/persons", tag="Person") 40 | api.route( 41 | PersonDetail, 42 | "person_detail", 43 | "/persons/", 44 | # "/computers//owner", 45 | tag="Person", 46 | ) 47 | api.route( 48 | PersonRelationship, 49 | "person_computers", 50 | "/persons//relationships/computers", 51 | ) 52 | 53 | api.route( 54 | ComputerList, "computer_list", "/computers", tag="Computer") 55 | api.route( 56 | ComputerDetail, "computer_detail", "/computers/", tag="Computer") 57 | 58 | 59 | @app.route("/") 60 | def hello(): 61 | db.create_all() # use for only for dev 62 | return {"message": "Hello world!"} 63 | 64 | 65 | if __name__ == "__main__": 66 | app.run() 67 | -------------------------------------------------------------------------------- /open-lessons/flask-jsonapi.20.08.2021/config.py: -------------------------------------------------------------------------------- 1 | class BaseConfig: 2 | SQLALCHEMY_DATABASE_URI = "sqlite:////:memory:" 3 | SQLALCHEMY_ECHO = False 4 | SQLALCHEMY_TRACK_MODIFICATIONS = False 5 | 6 | # swagger 7 | 8 | OPENAPI_URL_PREFIX = "/docs" 9 | OPENAPI_VERSION = "2.0.0" 10 | OPENAPI_SWAGGER_UI_PATH = "/" 11 | OPENAPI_SWAGGER_UI_VERSION = "3.45.0" 12 | 13 | 14 | class DevelopmentConfig(BaseConfig): 15 | DEBUG = True 16 | SQLALCHEMY_DATABASE_URI = "sqlite:///./flask-jsonapi.db" 17 | SQLALCHEMY_ECHO = True 18 | -------------------------------------------------------------------------------- /open-lessons/flask-jsonapi.20.08.2021/models/__init__.py: -------------------------------------------------------------------------------- 1 | from .person import Person 2 | from .computer import Computer 3 | 4 | __all__ = ( 5 | "Person", 6 | "Computer", 7 | ) 8 | -------------------------------------------------------------------------------- /open-lessons/flask-jsonapi.20.08.2021/models/computer.py: -------------------------------------------------------------------------------- 1 | from models.database import db 2 | 3 | 4 | class Computer(db.Model): 5 | id = db.Column(db.Integer, primary_key=True) 6 | serial = db.Column(db.String) 7 | person_id = db.Column(db.Integer, db.ForeignKey("person.id")) 8 | person = db.relationship("Person", back_populates="computers") 9 | -------------------------------------------------------------------------------- /open-lessons/flask-jsonapi.20.08.2021/models/database.py: -------------------------------------------------------------------------------- 1 | from flask_sqlalchemy import SQLAlchemy 2 | 3 | db = SQLAlchemy() 4 | -------------------------------------------------------------------------------- /open-lessons/flask-jsonapi.20.08.2021/models/person.py: -------------------------------------------------------------------------------- 1 | from datetime import date 2 | 3 | from .database import db 4 | 5 | 6 | class Person(db.Model): 7 | class Meta: 8 | required_fields = { 9 | "verbose_name": ["name", "birth_date"], 10 | } 11 | 12 | id = db.Column(db.Integer, primary_key=True) 13 | name = db.Column(db.String) 14 | birth_date = db.Column(db.Date) 15 | computers = db.relationship("Computer", back_populates="person") 16 | email = db.Column(db.String) 17 | 18 | @property 19 | def verbose_name(self): 20 | timedelta = date.today() - self.birth_date 21 | return f"{self.name} ({int(timedelta.days // 365.25)} y.o.)" 22 | -------------------------------------------------------------------------------- /open-lessons/flask-jsonapi.20.08.2021/requirements.txt: -------------------------------------------------------------------------------- 1 | apispec==4.7.1 2 | click==8.0.1 3 | ComboJSONAPI==1.1.1 4 | Flask==1.1.2 5 | Flask-COMBO-JSONAPI==1.0.7 6 | Flask-SQLAlchemy==2.5.1 7 | itsdangerous==2.0.1 8 | Jinja2==3.0.1 9 | MarkupSafe==2.0.1 10 | marshmallow==3.2.1 11 | marshmallow-jsonapi==0.24.0 12 | PyYAML==5.4.1 13 | simplejson==3.17.3 14 | SQLAlchemy==1.3.24 15 | Werkzeug==2.0.1 16 | -------------------------------------------------------------------------------- /open-lessons/flask-jsonapi.20.08.2021/schemas/__init__.py: -------------------------------------------------------------------------------- 1 | from .person import PersonSchema 2 | from .computer import ComputerSchema 3 | 4 | __all__ = ( 5 | "PersonSchema", 6 | "ComputerSchema", 7 | ) 8 | -------------------------------------------------------------------------------- /open-lessons/flask-jsonapi.20.08.2021/schemas/computer.py: -------------------------------------------------------------------------------- 1 | from marshmallow_jsonapi import fields 2 | from marshmallow_jsonapi.flask import Schema 3 | 4 | from combojsonapi.utils import Relationship 5 | 6 | 7 | class ComputerSchema(Schema): 8 | class Meta: 9 | type_ = "computer" 10 | self_view = "computer_detail" 11 | self_view_kwargs = {"id": ""} 12 | self_view_many = "computer_list" 13 | 14 | id = fields.Integer(as_string=True, dump_only=True) 15 | serial = fields.String(required=True) 16 | owner = Relationship( 17 | type_="person", 18 | attribute="person", 19 | schema="PersonSchema", 20 | nested="PersonSchema", 21 | related_view="person_detail", 22 | related_view_kwargs={"id": ""}, 23 | self_view="computer_person", 24 | self_view_kwargs={"id": ""}, 25 | ) 26 | -------------------------------------------------------------------------------- /open-lessons/flask-jsonapi.20.08.2021/schemas/person.py: -------------------------------------------------------------------------------- 1 | from marshmallow_jsonapi import fields 2 | from marshmallow_jsonapi.flask import Schema 3 | from combojsonapi.utils import Relationship 4 | 5 | 6 | class PersonSchema(Schema): 7 | class Meta: 8 | type_ = "person" 9 | self_view_many = "person_list" 10 | self_view = "person_detail" 11 | self_view_kwargs = {"id": ""} 12 | 13 | id = fields.Integer(as_string=True) 14 | name = fields.String() 15 | birth_date = fields.Date() 16 | verbose_name = fields.String(dump_only=True) 17 | email = fields.Email() 18 | computers = Relationship( 19 | type_="computer", 20 | schema="ComputerSchema", 21 | nested="ComputerSchema", 22 | many=True, 23 | related_view="computer_list", 24 | # related_view_kwargs={"id": ""}, 25 | self_view="person_computers", 26 | self_view_kwargs={"id": ""}, 27 | ) 28 | -------------------------------------------------------------------------------- /open-lessons/flask-jsonapi.20.08.2021/views/__init__.py: -------------------------------------------------------------------------------- 1 | from .person import PersonList, PersonDetail, PersonRelationship 2 | from .computer import ComputerList, ComputerDetail 3 | 4 | __all__ = ( 5 | "PersonList", 6 | "PersonDetail", 7 | "PersonRelationship", 8 | "ComputerList", 9 | "ComputerDetail", 10 | ) 11 | -------------------------------------------------------------------------------- /open-lessons/flask-jsonapi.20.08.2021/views/computer.py: -------------------------------------------------------------------------------- 1 | from flask_combo_jsonapi import ResourceList, ResourceDetail 2 | 3 | 4 | from models import Computer 5 | from models.database import db 6 | from schemas import ComputerSchema 7 | from .permissions.computer import ComputerPermission 8 | 9 | 10 | class ComputerList(ResourceList): 11 | schema = ComputerSchema 12 | data_layer = { 13 | "session": db.session, 14 | "model": Computer, 15 | "permission_get": [ComputerPermission], 16 | } 17 | 18 | 19 | class ComputerDetail(ResourceDetail): 20 | schema = ComputerSchema 21 | data_layer = { 22 | "session": db.session, 23 | "model": Computer, 24 | "permission_get": [ComputerPermission], 25 | } 26 | -------------------------------------------------------------------------------- /open-lessons/flask-jsonapi.20.08.2021/views/permissions/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OtusTeam/BasePython/d87ee93755f4c2dbb4c2b4bbd35743a2c6e44043/open-lessons/flask-jsonapi.20.08.2021/views/permissions/__init__.py -------------------------------------------------------------------------------- /open-lessons/flask-jsonapi.20.08.2021/views/permissions/base.py: -------------------------------------------------------------------------------- 1 | from combojsonapi.permission import PermissionMixin, PermissionForGet, PermissionUser 2 | 3 | 4 | class BaseAllowGetAllPermission(PermissionMixin): 5 | ALL_FIELDS = [] 6 | 7 | def get( 8 | self, 9 | *args, 10 | many=True, 11 | user_permission: PermissionUser = None, 12 | **kwargs, 13 | ) -> PermissionForGet: 14 | """ 15 | Allow all the declared columns 16 | """ 17 | self.permission_for_get.allow_columns = (self.ALL_FIELDS, 10) 18 | return self.permission_for_get 19 | -------------------------------------------------------------------------------- /open-lessons/flask-jsonapi.20.08.2021/views/permissions/computer.py: -------------------------------------------------------------------------------- 1 | from .base import BaseAllowGetAllPermission 2 | 3 | 4 | class ComputerPermission(BaseAllowGetAllPermission): 5 | ALL_FIELDS = [ 6 | "id", 7 | "serial", 8 | "owner", 9 | ] 10 | -------------------------------------------------------------------------------- /open-lessons/flask-jsonapi.20.08.2021/views/permissions/person.py: -------------------------------------------------------------------------------- 1 | from .base import BaseAllowGetAllPermission 2 | 3 | 4 | class PersonPermission(BaseAllowGetAllPermission): 5 | ALL_FIELDS = [ 6 | "id", 7 | "name", 8 | "birth_date", 9 | # "email", 10 | "verbose_name", 11 | "computers", 12 | ] 13 | -------------------------------------------------------------------------------- /open-lessons/flask-jsonapi.20.08.2021/views/person.py: -------------------------------------------------------------------------------- 1 | from combojsonapi.event import EventPlugin 2 | from flask_combo_jsonapi import ResourceList, ResourceDetail, ResourceRelationship 3 | from flask_combo_jsonapi.data_layers.alchemy import SqlalchemyDataLayer 4 | from flask_combo_jsonapi.exceptions import ObjectNotFound 5 | from sqlalchemy.orm.exc import NoResultFound 6 | 7 | from models import Person, Computer 8 | from models.database import db 9 | from schemas import PersonSchema 10 | from views.permissions.person import PersonPermission 11 | 12 | 13 | class PersonDetailSqlalchemyDataLayer(SqlalchemyDataLayer): 14 | 15 | def before_get_object(self, view_kwargs): 16 | if not view_kwargs.get("computer_id"): 17 | return 18 | 19 | try: 20 | # result = self.session.query(Computer).values("id", "person_id").filter_by( 21 | computer = self.session.query(Computer).filter_by( 22 | id=view_kwargs["computer_id"], 23 | ).one() 24 | except NoResultFound: 25 | raise ObjectNotFound( 26 | "Computer: {} not found".format(view_kwargs["computer_id"]), 27 | source={"parameter": 'computer_id'}, 28 | ) 29 | else: 30 | view_kwargs["id"] = computer.person_id 31 | 32 | 33 | class PersonListEvents(EventPlugin): 34 | def event_get_info(self): 35 | return {"message": "Hello Person List events!"} 36 | 37 | def event_refresh_persons(self): 38 | return {"message": "Persons refreshed!"} 39 | 40 | 41 | class PersonList(ResourceList): 42 | schema = PersonSchema 43 | events = PersonListEvents 44 | # methods = ["GET"] 45 | data_layer = { 46 | "session": db.session, 47 | "model": Person, 48 | "permission_get": [PersonPermission], 49 | # "permission_post": [PersonPermission], 50 | } 51 | 52 | 53 | class PersonDetailEvents(EventPlugin): 54 | def event_update_online_status(self, *args, **kwargs): 55 | # language=YAML 56 | """ 57 | --- 58 | summary: Update person's online status 59 | tags: 60 | - Person 61 | parameters: 62 | - in: path 63 | name: id 64 | required: True 65 | type: integer 66 | format: int32 67 | description: 'person id' 68 | consumes: 69 | - application/json 70 | responses: 71 | 200: 72 | description: update status 73 | """ 74 | # some swagger description 75 | person_id = kwargs["id"] 76 | # person update status 77 | return {"status": "updated", "person_id": person_id} 78 | 79 | event_update_online_status.extra = { 80 | "url_suffix": "update-online", 81 | "method": "PUT", 82 | } 83 | 84 | 85 | class PersonDetail(ResourceDetail): 86 | schema = PersonSchema 87 | events = PersonDetailEvents 88 | # methods = ["GET", "PATCH"] 89 | # methods = ["GET"] 90 | data_layer = { 91 | "class": PersonDetailSqlalchemyDataLayer, 92 | "session": db.session, 93 | "model": Person, 94 | "permission_get": [PersonPermission], 95 | # "permission_patch": [PersonPermission], 96 | # "permission_put": [PersonPermission], 97 | # "permission_delete": [PersonPermission], 98 | } 99 | 100 | 101 | class PersonRelationship(ResourceRelationship): 102 | schema = PersonSchema 103 | data_layer = { 104 | 'session': db.session, 105 | 'model': Person 106 | } 107 | 108 | -------------------------------------------------------------------------------- /open-lessons/flask.23.05.2022/main.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, request 2 | 3 | from views.products import products_app 4 | 5 | app = Flask(__name__) 6 | app.config.update(ENV="development") 7 | 8 | app.register_blueprint( 9 | products_app, 10 | url_prefix="/products", 11 | ) 12 | 13 | 14 | @app.route("/") 15 | def hello_world(): 16 | # another_func() 17 | return "

Hello, World!!

" 18 | 19 | 20 | def another_func(): 21 | print(request) 22 | print(request.path) 23 | 24 | 25 | @app.route("/hello/") 26 | def hello_view(): 27 | # another_func() 28 | name = request.args.get("name", "World") 29 | name = name.strip() 30 | if not name: 31 | name = "World" 32 | return {"message": f"Hello {name}!"} 33 | 34 | 35 | @app.route("/items//") 36 | def get_item(item_id: int): 37 | return {"item_id": item_id} 38 | 39 | 40 | @app.route("/items//") 41 | def get_item_str(item_id: str): 42 | return {"item": {"id": item_id.lower()}} 43 | 44 | 45 | if __name__ == '__main__': 46 | app.run(debug=True) 47 | -------------------------------------------------------------------------------- /open-lessons/flask.23.05.2022/requirements.txt: -------------------------------------------------------------------------------- 1 | click==8.1.3 2 | Flask==2.1.2 3 | itsdangerous==2.1.2 4 | Jinja2==3.1.2 5 | MarkupSafe==2.1.1 6 | Werkzeug==2.1.2 7 | -------------------------------------------------------------------------------- /open-lessons/flask.23.05.2022/templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | {% block title %} 7 | Base Title 8 | {% endblock %} 9 | 10 | 11 | 12 | {% block body %} 13 | Base Body 14 | {# #} 15 | {# {% block small %}#} 16 | {# small text#} 17 | {# {% endblock %}#} 18 | {# #} 19 | {% endblock %} 20 | 21 |
22 | 23 | OTUS © 2022 24 | 25 |
26 | 27 | -------------------------------------------------------------------------------- /open-lessons/flask.23.05.2022/templates/products/add.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block body %} 4 |
5 |
6 | 7 | 8 | 9 | 10 |
11 |
12 | {% endblock %} 13 | 14 | {% block title %} 15 | Add product 16 | {% endblock %} 17 | -------------------------------------------------------------------------------- /open-lessons/flask.23.05.2022/templates/products/details.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block title %} 4 | Product #{{ product_id }} 5 | {% endblock %} 6 | 7 | {% block body %} 8 |

Product {{ product_name }}

9 | 10 | 15 | {% endblock %} 16 | -------------------------------------------------------------------------------- /open-lessons/flask.23.05.2022/templates/products/list.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block title %} 4 | Products List 5 | {% endblock %} 6 | 7 | {% block body %} 8 |

Products list:

9 |
10 | 19 |
20 | 24 | {% endblock %} 25 | 26 | 27 | {#{% block small %}#} 28 | {#some products#} 29 | {#{% endblock %}#} -------------------------------------------------------------------------------- /open-lessons/flask.23.05.2022/views/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OtusTeam/BasePython/d87ee93755f4c2dbb4c2b4bbd35743a2c6e44043/open-lessons/flask.23.05.2022/views/__init__.py -------------------------------------------------------------------------------- /open-lessons/flask.23.05.2022/views/products.py: -------------------------------------------------------------------------------- 1 | from flask import ( 2 | Blueprint, 3 | render_template, 4 | request, 5 | redirect, 6 | url_for, 7 | ) 8 | from werkzeug.exceptions import BadRequest, NotFound 9 | 10 | products_app = Blueprint("products_app", __name__) 11 | 12 | PRODUCTS = { 13 | 1: "Laptop", 14 | 2: "Desktop", 15 | 3: "Smartphone", 16 | } 17 | 18 | 19 | @products_app.route("/", endpoint="list") 20 | def products_list(): 21 | return render_template( 22 | "products/list.html", 23 | products=list(PRODUCTS.items()), 24 | ) 25 | 26 | 27 | @products_app.route("//", endpoint="details") 28 | def get_product(product_id: int): 29 | product_name = PRODUCTS.get(product_id) 30 | if product_name is None: 31 | raise NotFound(f"product #{product_id} doesn't exist!") 32 | return render_template( 33 | "products/details.html", 34 | product_id=product_id, 35 | product_name=product_name, 36 | ) 37 | 38 | 39 | @products_app.route("/add/", methods=["GET", "POST"], endpoint="add") 40 | def add_product(): 41 | if request.method == "GET": 42 | return render_template("products/add.html") 43 | 44 | # if request.method == "POST": 45 | # pass 46 | 47 | product_name = request.form.get("product-name") 48 | if not product_name: 49 | raise BadRequest("Field `product-name` required!") 50 | 51 | product_id = len(PRODUCTS) + 1 52 | PRODUCTS[product_id] = product_name 53 | url = url_for("products_app.details", product_id=product_id) 54 | return redirect(url) 55 | -------------------------------------------------------------------------------- /open-lessons/helper-funcs.09.02.2023/requirements.txt: -------------------------------------------------------------------------------- 1 | jupyter==1.0.0 2 | jupyter-console==6.4.4 3 | jupyter-events==0.6.3 4 | jupyter_client==8.0.2 5 | jupyter_core==5.2.0 6 | jupyter_server==2.2.1 7 | jupyter_server_terminals==0.4.4 8 | jupyterlab-pygments==0.2.2 9 | jupyterlab-widgets==3.0.5 10 | -------------------------------------------------------------------------------- /open-lessons/helper-funcs.21.02.2022/requirements.txt: -------------------------------------------------------------------------------- 1 | appnope==0.1.2 2 | argon2-cffi==21.3.0 3 | argon2-cffi-bindings==21.2.0 4 | asttokens==2.0.5 5 | attrs==21.4.0 6 | backcall==0.2.0 7 | black==22.1.0 8 | bleach==4.1.0 9 | cffi==1.15.0 10 | click==8.0.4 11 | debugpy==1.5.1 12 | decorator==5.1.1 13 | defusedxml==0.7.1 14 | entrypoints==0.4 15 | executing==0.8.2 16 | ipykernel==6.9.1 17 | ipython==8.0.1 18 | ipython-genutils==0.2.0 19 | jedi==0.18.1 20 | Jinja2==3.0.3 21 | jsonschema==4.4.0 22 | jupyter-client==7.1.2 23 | jupyter-core==4.9.2 24 | jupyterlab-pygments==0.1.2 25 | MarkupSafe==2.1.0 26 | matplotlib-inline==0.1.3 27 | mistune==0.8.4 28 | mypy-extensions==0.4.3 29 | nbclient==0.5.11 30 | nbconvert==6.4.2 31 | nbformat==5.1.3 32 | nest-asyncio==1.5.4 33 | notebook==6.4.8 34 | packaging==21.3 35 | pandocfilters==1.5.0 36 | parso==0.8.3 37 | pathspec==0.9.0 38 | pexpect==4.8.0 39 | pickleshare==0.7.5 40 | platformdirs==2.5.1 41 | prometheus-client==0.13.1 42 | prompt-toolkit==3.0.28 43 | ptyprocess==0.7.0 44 | pure-eval==0.2.2 45 | pycparser==2.21 46 | Pygments==2.11.2 47 | pyparsing==3.0.7 48 | pyrsistent==0.18.1 49 | python-dateutil==2.8.2 50 | pyzmq==22.3.0 51 | Send2Trash==1.8.0 52 | six==1.16.0 53 | stack-data==0.2.0 54 | terminado==0.13.1 55 | testpath==0.5.0 56 | tomli==2.0.1 57 | tornado==6.1 58 | traitlets==5.1.1 59 | typing_extensions==4.1.1 60 | wcwidth==0.2.5 61 | webencodings==0.5.1 62 | -------------------------------------------------------------------------------- /open-lessons/interfaces-and-protocols-27.08/file.txt: -------------------------------------------------------------------------------- 1 | some file text 2 | -------------------------------------------------------------------------------- /open-lessons/interfaces-and-protocols-27.08/interfaces/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OtusTeam/BasePython/d87ee93755f4c2dbb4c2b4bbd35743a2c6e44043/open-lessons/interfaces-and-protocols-27.08/interfaces/__init__.py -------------------------------------------------------------------------------- /open-lessons/interfaces-and-protocols-27.08/interfaces/light_bulb.py: -------------------------------------------------------------------------------- 1 | from .switchable import Switchable 2 | 3 | 4 | class LightBulb(Switchable): 5 | def on(self): 6 | print("light bulb on") 7 | 8 | def off(self): 9 | print("light bulb off") 10 | -------------------------------------------------------------------------------- /open-lessons/interfaces-and-protocols-27.08/interfaces/power_switch.py: -------------------------------------------------------------------------------- 1 | from typing import TYPE_CHECKING 2 | 3 | if TYPE_CHECKING: 4 | from .switchable import Switchable 5 | 6 | 7 | class PowerSwitch: 8 | def __init__(self, client: "Switchable"): 9 | self.client = client 10 | self.on = False 11 | self.client.off() 12 | 13 | def toggle(self): 14 | if self.on: 15 | self.client.off() 16 | self.on = False 17 | else: 18 | self.client.on() 19 | self.on = True 20 | -------------------------------------------------------------------------------- /open-lessons/interfaces-and-protocols-27.08/interfaces/switchable.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | 3 | 4 | class Switchable(ABC): 5 | @abstractmethod 6 | def on(self): 7 | raise NotImplementedError 8 | 9 | @abstractmethod 10 | def off(self): 11 | raise NotImplementedError 12 | -------------------------------------------------------------------------------- /open-lessons/interfaces-and-protocols-27.08/interfaces/wireless_mouse.py: -------------------------------------------------------------------------------- 1 | from .switchable import Switchable 2 | 3 | 4 | class WirelessMouse(Switchable): 5 | def on(self): 6 | print("mouse on") 7 | 8 | def off(self): 9 | print("mouse off") 10 | 11 | def track(self): 12 | print("mouse tracking") 13 | 14 | def click(self): 15 | print("mouse clicking") 16 | -------------------------------------------------------------------------------- /open-lessons/interfaces-and-protocols-27.08/main.py: -------------------------------------------------------------------------------- 1 | from interfaces.switchable import Switchable 2 | from interfaces.light_bulb import LightBulb 3 | from interfaces.wireless_mouse import WirelessMouse 4 | from interfaces.power_switch import PowerSwitch 5 | 6 | 7 | class Oven(Switchable): 8 | def off(self): 9 | print("oven off") 10 | 11 | def on(self): 12 | print("oven on") 13 | 14 | 15 | def example_interfaces(): 16 | # sw = Switchable() 17 | # print("SW:", sw) 18 | bulb = LightBulb() 19 | print("bulb:", bulb) 20 | bulb.on() 21 | bulb.off() 22 | 23 | mouse = WirelessMouse() 24 | print("mouse:", mouse) 25 | mouse.on() 26 | mouse.off() 27 | 28 | switch = PowerSwitch(client=bulb) 29 | switch.toggle() 30 | switch.toggle() 31 | switch.toggle() 32 | 33 | mouse_switch = PowerSwitch(client=mouse) 34 | mouse_switch.toggle() 35 | mouse_switch.toggle() 36 | mouse_switch.toggle() 37 | 38 | oven = Oven() 39 | oven_switch = PowerSwitch(client=oven) 40 | oven_switch.toggle() 41 | 42 | 43 | def main(): 44 | example_interfaces() 45 | 46 | 47 | if __name__ == "__main__": 48 | main() 49 | -------------------------------------------------------------------------------- /open-lessons/interfaces-and-protocols-27.08/protocols/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OtusTeam/BasePython/d87ee93755f4c2dbb4c2b4bbd35743a2c6e44043/open-lessons/interfaces-and-protocols-27.08/protocols/__init__.py -------------------------------------------------------------------------------- /open-lessons/interfaces-and-protocols-27.08/protocols/example_message_receiver.py: -------------------------------------------------------------------------------- 1 | from typing import Protocol 2 | 3 | 4 | class CallbackProtocol(Protocol): 5 | __name__: str 6 | 7 | def __call__( 8 | self, 9 | message: str, 10 | size: int, 11 | ) -> None: ... 12 | 13 | 14 | class Callback: 15 | 16 | @property 17 | def __name__(self): 18 | return self.__class__.__name__ 19 | 20 | def __call__(self, message: str, size: int) -> None: 21 | print("(cls) received message:", message, "of size", size) 22 | 23 | 24 | def process_incoming_message( 25 | data: bytes, 26 | callback: CallbackProtocol, 27 | ) -> None: 28 | message = data.decode("utf-8") 29 | message_size = len(message) 30 | print("notify callback", callback.__name__) 31 | callback( 32 | message=message, 33 | size=message_size, 34 | ) 35 | callback( 36 | message, 37 | size=message_size, 38 | ) 39 | callback( 40 | message, 41 | message_size, 42 | ) 43 | 44 | 45 | def my_callback(message: str, size: int) -> None: 46 | print("received message:", message, "of size", size) 47 | 48 | 49 | def main() -> None: 50 | data = b"Hello World!" 51 | process_incoming_message( 52 | data=data, 53 | callback=my_callback, 54 | ) 55 | callback = Callback() 56 | 57 | process_incoming_message( 58 | data=data, 59 | # беда 60 | callback=callback, 61 | ) 62 | 63 | 64 | if __name__ == "__main__": 65 | main() 66 | -------------------------------------------------------------------------------- /open-lessons/interfaces-and-protocols-27.08/protocols/example_readable.py: -------------------------------------------------------------------------------- 1 | from typing import Protocol 2 | 3 | 4 | class Readable(Protocol): 5 | def read(self) -> bytes: ... 6 | 7 | 8 | class FileReader: 9 | def __init__(self, filename: str): 10 | self.filename = filename 11 | 12 | def read(self) -> bytes: 13 | """ 14 | Тут мы читаем файл 15 | """ 16 | print("reading file", self.filename) 17 | return self.filename.encode() 18 | 19 | 20 | class APIReader: 21 | def __init__(self, api_path: str): 22 | self.api_path = api_path 23 | 24 | def read(self) -> bytes: 25 | """ 26 | Тут мы читаем внешний API 27 | """ 28 | print("reading api path", self.api_path) 29 | return self.api_path.encode() 30 | 31 | 32 | def read_data_to_db(readable: Readable): 33 | print("save to db data:", readable.read()) 34 | 35 | 36 | def main(): 37 | file_reader = FileReader("some-filename.txt") 38 | api_reader = APIReader("some/api/path") 39 | file = open( 40 | "/Users/suren/MyFiles/OTUS/lessons/open-lessons/py-basic-proto-and-interfaces-27.08.2024/file.txt", 41 | mode="rb", 42 | ) 43 | read_data_to_db(file_reader) 44 | read_data_to_db(api_reader) 45 | read_data_to_db(file) 46 | 47 | 48 | if __name__ == "__main__": 49 | main() 50 | -------------------------------------------------------------------------------- /open-lessons/interfaces-and-protocols-27.08/protocols/example_runtime_checkable_protocol.py: -------------------------------------------------------------------------------- 1 | from typing import Protocol, runtime_checkable 2 | 3 | 4 | @runtime_checkable 5 | class Draggable(Protocol): 6 | weight: int 7 | handles: int 8 | 9 | 10 | class Bag: 11 | def __init__(self, weight: int, handles: int = 2) -> None: 12 | self.weight = weight 13 | self.handles = handles 14 | 15 | 16 | class SuitCase: 17 | def __init__(self, weight: int, handles: int = 1) -> None: 18 | self.weight = weight 19 | self.handles = handles 20 | 21 | 22 | class Cat: 23 | def __init__(self, name: str, weight: int) -> None: 24 | self.name = name 25 | self.weight = weight 26 | 27 | 28 | def drag(item: Draggable) -> None: 29 | print("Drag", item.weight, "kg using", item.handles, "handles") 30 | 31 | 32 | def main(): 33 | bag = Bag(15) 34 | case = SuitCase(7) 35 | cat = Cat("Cat", 5) 36 | elements = [bag, case, cat] 37 | for el in elements: 38 | if isinstance(el, Draggable): 39 | drag(el) 40 | else: 41 | print(el, "is not draggable") 42 | 43 | 44 | if __name__ == "__main__": 45 | main() 46 | -------------------------------------------------------------------------------- /open-lessons/interfaces-and-protocols-27.08/requirements.txt: -------------------------------------------------------------------------------- 1 | mypy==1.11.2 2 | mypy-extensions==1.0.0 3 | typing_extensions==4.12.2 4 | -------------------------------------------------------------------------------- /open-lessons/oop.18.08.2022/step_1.py: -------------------------------------------------------------------------------- 1 | """ 2 | This is a documentation string 3 | """ 4 | 5 | some_object = object() 6 | print(some_object) 7 | 8 | a = 1 9 | print(type(a)) 10 | b = '' 11 | print(type(b)) 12 | 13 | c = int() 14 | d = str() 15 | 16 | print(type(c)) 17 | print(type(d)) 18 | 19 | 20 | # class User(object): 21 | class User: 22 | """ 23 | Doc string for user 24 | """ 25 | 26 | 27 | print(int) 28 | print(User) 29 | 30 | user1 = User() 31 | print(user1) 32 | print(type(user1)) 33 | 34 | user1.name = "John" 35 | print("user name:", user1.name) 36 | print("user1 dict", user1.__dict__) 37 | 38 | user2 = User() 39 | user2.name = "Sam" 40 | print("user2 name", user2.name) 41 | print("user2 dict", user2.__dict__) 42 | user2.age = 20 43 | print("user2 age", user2.age) 44 | print("user2 dict", user2.__dict__) 45 | 46 | print("user1 dict", user1.__dict__) 47 | print(User.mro()) 48 | 49 | # print("user1 age", user1.age) 50 | 51 | 52 | # print("__name__:", __name__) 53 | 54 | -------------------------------------------------------------------------------- /open-lessons/oop.18.08.2022/step_2.py: -------------------------------------------------------------------------------- 1 | class User: 2 | name = "John" 3 | age = "30" 4 | 5 | 6 | """ 7 | # дескрипторы: поиск атрибутов на объектах 8 | - на экземпляре 9 | - на классе экземпляра 10 | - на родителях класса (типа) 11 | - на родителях родителя (до самого верха -- object) 12 | !- если не найдено - ошибка! 13 | """ 14 | 15 | print("User name", User.name) 16 | print("User age", User.age) 17 | 18 | 19 | user1 = User() 20 | print(User.mro()) 21 | print("user1 name", user1.name) 22 | print("user1 age", user1.age) 23 | print("user1 dict", user1.__dict__) 24 | 25 | user1.name = "Nick" 26 | print("user1 name", user1.name) 27 | print("user1 dict", user1.__dict__) 28 | print("User name", User.name) 29 | 30 | user2 = User() 31 | print("user2 name", user2.name) 32 | 33 | User.name = "Bob" 34 | print("user1 name", user1.name) 35 | print("user2 name", user2.name) 36 | 37 | -------------------------------------------------------------------------------- /open-lessons/oop.18.08.2022/step_3.py: -------------------------------------------------------------------------------- 1 | class User: 2 | def __init__(self): 3 | self.name = None 4 | self.age = None 5 | 6 | 7 | # def init(user): 8 | # user.name = None 9 | # user.age = None 10 | 11 | 12 | user1 = User() 13 | # init(user1) 14 | print(user1.__dict__) 15 | 16 | -------------------------------------------------------------------------------- /open-lessons/oop.18.08.2022/step_4.py: -------------------------------------------------------------------------------- 1 | class Product: 2 | def __init__(self, name, price): 3 | self.name = name 4 | self.price = price 5 | 6 | def __str__(self): 7 | return f"Product(name={self.name!r}, price={self.price})" 8 | 9 | def __repr__(self): 10 | return str(self) 11 | # return "Product" 12 | 13 | def make_discount(self, discount_percent): 14 | """ 15 | Applies discount in % 16 | :param discount_percent: 17 | :return: 18 | """ 19 | self.price *= (100 - discount_percent) / 100 20 | 21 | 22 | # def init_product(product, name, price): 23 | # product.name = name 24 | # product.price = price 25 | # 26 | # 27 | # Product.__init__ = init_product 28 | 29 | product_laptop = Product("Laptop 1", 2000) 30 | # init_product(product_laptop, "Laptop 2", 2999) 31 | 32 | print(product_laptop.name) 33 | print(repr(product_laptop.name)) 34 | print(product_laptop.price) 35 | print(product_laptop.__dict__) 36 | 37 | print(type(product_laptop)) 38 | print(product_laptop) 39 | print(repr(product_laptop)) 40 | 41 | 42 | product_smartphone = Product("Smartphone 1", 1000) 43 | 44 | print(product_smartphone) 45 | print(product_laptop) 46 | 47 | product_smartphone.make_discount(10) 48 | product_laptop.make_discount(20) 49 | 50 | print(product_smartphone) 51 | print(product_laptop) 52 | -------------------------------------------------------------------------------- /open-lessons/oop.18.08.2022/step_5.py: -------------------------------------------------------------------------------- 1 | class Date: 2 | def __init__(self, year, month, day): 3 | self.year = year 4 | self.month = month 5 | self.day = day 6 | # self.__demo_hidden = "super secret!!" 7 | 8 | def copy(self): 9 | return Date(year=self.year, month=self.month, day=self.day) 10 | 11 | @classmethod 12 | def from_date_string(cls, date_string: str): 13 | """ 14 | Parse date from string '2022-08-18' 15 | 16 | :param date_string: 17 | :return: 18 | """ 19 | year, month, day = map(int, date_string.split("-")) 20 | date = cls(year=year, month=month, day=day) 21 | return date 22 | 23 | @staticmethod 24 | def is_date_string_valid(date_string: str): 25 | if date_string.count("-") != 2: 26 | return False 27 | 28 | year, month, day = map(int, date_string.split("-")) 29 | return day <= 31 and month <= 12 and year <= 3999 30 | 31 | def __str__(self): 32 | return ( 33 | f"{self.__class__.__name__}(year={self.year}, " 34 | f"month={self.month}, day={self.day})" 35 | ) 36 | 37 | 38 | # Date.is_date_string_valid() 39 | 40 | 41 | print(Date) 42 | print(Date.__name__) 43 | 44 | 45 | date1 = Date(2022, 8, 18) 46 | date2 = Date(1934, 3, 24) 47 | 48 | print(date1) 49 | print(date2) 50 | 51 | date3 = date1 52 | print(date3) 53 | print(date1) 54 | date3.month = 1 55 | date1.year = 2000 56 | 57 | print(date3) 58 | print(date1) 59 | 60 | date4 = date1.copy() 61 | print("---") 62 | print(date1) 63 | print(date4) 64 | 65 | date1.year = 2002 66 | date4.day = 22 67 | date4.month = 2 68 | print(date1) 69 | print(date4) 70 | 71 | print('***') 72 | 73 | # date5 = date4.from_date_string("") 74 | date5 = Date.from_date_string("2048-11-22") 75 | print(date5) 76 | 77 | print(Date.is_date_string_valid("200")) 78 | print(Date.is_date_string_valid("2048-11-22")) 79 | print(Date.is_date_string_valid("4048-11-22")) 80 | print(Date.is_date_string_valid("2048-11-31")) 81 | 82 | # Date.copy(date4) 83 | # date4.copy() 84 | 85 | 86 | # print(date1.__dict__) 87 | # # print(date1.__demo_hidden) 88 | # print(date2._Date__demo_hidden) 89 | # date2._Date__demo_hidden = "not secret!" 90 | # print(date2._Date__demo_hidden) 91 | # print(date2.__dict__) 92 | -------------------------------------------------------------------------------- /open-lessons/oop.25.04.2023/step0.py: -------------------------------------------------------------------------------- 1 | print("Hello world!") 2 | 3 | some_object = object() 4 | 5 | print(some_object) 6 | 7 | 8 | def my_func(): 9 | print("hello") 10 | # return "abc" 11 | # return None 12 | # return 13 | 14 | 15 | print(my_func) 16 | 17 | res = my_func() 18 | print("result:", res) 19 | 20 | 21 | class MyObject(object): 22 | pass 23 | 24 | 25 | print(object) 26 | print(MyObject) 27 | my_obj = MyObject() 28 | print(some_object) 29 | print(my_obj) 30 | 31 | print("MyObject mro", MyObject.mro()) 32 | 33 | 34 | class User: 35 | pass 36 | 37 | 38 | user = User() 39 | print(User) 40 | print(user) 41 | print("user type:", type(user)) 42 | print("User type:", type(User)) 43 | 44 | print("User mro", User.mro()) 45 | 46 | line_hello = "hello" 47 | # line_hello = str("hello") 48 | print(line_hello) 49 | line_hello_type = type(line_hello) 50 | print(line_hello_type) 51 | print(line_hello_type is str) 52 | print("str type:", type(str)) 53 | print("type is type", type(str) is type) 54 | print("type mro", type.mro(type)) 55 | print("type object", type(object)) 56 | 57 | 58 | john = User() 59 | print("john dict", john.__dict__) 60 | # print("john name", john.name) 61 | john.name = "John" 62 | print("john:", john, john.name) 63 | print("john dict", john.__dict__) 64 | 65 | 66 | sam = User() 67 | print("sam dict", sam.__dict__) 68 | 69 | sam.username = "sam3000" 70 | print("sam:", sam, sam.username) 71 | # print("sam:", sam, sam.name) 72 | 73 | print("john dict", john.__dict__) 74 | print("sam dict", sam.__dict__) 75 | -------------------------------------------------------------------------------- /open-lessons/oop.25.04.2023/step1.py: -------------------------------------------------------------------------------- 1 | class User: 2 | name = None 3 | username = None 4 | age = None 5 | 6 | 7 | sam = User() 8 | print("sam:", sam, sam.name, sam.age) 9 | print("sam dict:", sam.__dict__) 10 | 11 | john = User() 12 | print(john, john.name, john.age) 13 | print("john dict:", john.__dict__) 14 | 15 | print("dict eq?", john.__dict__ == sam.__dict__) 16 | print("dict same object?", john.__dict__ is sam.__dict__) 17 | john.name = "John" 18 | john.age = 42 19 | print(john, john.name, john.age) 20 | print(john.__dict__) 21 | 22 | print("sam:", sam, sam.name, sam.age) 23 | 24 | print("___") 25 | print("sam dict:", sam.__dict__) 26 | print("john dict:", john.__dict__) 27 | 28 | print("sam username", sam.username) 29 | print("john username", john.username) 30 | 31 | 32 | print(User.__dict__) 33 | User.username = "default-username" 34 | print(User.__dict__) 35 | 36 | print("sam username", sam.username) 37 | print("john username", john.username) 38 | 39 | print("sam dict:", sam.__dict__) 40 | print("john dict:", john.__dict__) 41 | -------------------------------------------------------------------------------- /open-lessons/oop.25.04.2023/step2.py: -------------------------------------------------------------------------------- 1 | class User: 2 | def __init__( 3 | self, 4 | name: str, 5 | username: str | None = None, 6 | age: int | None = None, 7 | ): 8 | self.name = name 9 | self.username = username 10 | self.age = age 11 | 12 | def increase_age(self): 13 | if self.age is not None: 14 | self.age += 1 15 | else: 16 | self.age = 0 17 | 18 | 19 | print(User.__dict__) 20 | 21 | john = User(name="John") 22 | print("john", john, john.__dict__) 23 | 24 | sam = User(name="Sam", username="sam3000") 25 | print(sam.name) 26 | print("sam", sam, sam.__dict__) 27 | print("sam name:", sam.name) 28 | print("sam username:", sam.username) 29 | # sam.username = "sam3000" 30 | # print("sam", sam, sam.__dict__) 31 | 32 | print(sam.age) 33 | sam.increase_age() 34 | print(sam.age) 35 | sam.increase_age() 36 | print(sam.age) 37 | sam.increase_age() 38 | print(sam.age) 39 | 40 | -------------------------------------------------------------------------------- /open-lessons/oop.25.04.2023/step3.py: -------------------------------------------------------------------------------- 1 | class Shape: 2 | 3 | def get_area(self): 4 | print("not enough data") 5 | # return None 6 | 7 | 8 | class Rectangle(Shape): 9 | def __init__(self, a, b): 10 | self.a = a 11 | self.b = b 12 | 13 | # overload - (перегрузка) не бывает в Python 14 | # def method(self, a): 15 | # return a ** 2 16 | # 17 | # def method(self, a, b): 18 | # return a ** b 19 | # 20 | # def method(self, a, b, c): 21 | # return a * b * c 22 | 23 | # override - переопределение 24 | def get_area(self): 25 | return self.a * self.b 26 | 27 | def __str__(self): 28 | return f"{self.__class__.__name__}(a={self.a}, b={self.b})" 29 | 30 | 31 | class Square(Rectangle): 32 | def __init__(self, a): 33 | self.a = a 34 | # self.b = a 35 | 36 | @property 37 | def b(self): 38 | return self.a 39 | 40 | @b.setter 41 | def b(self, value): 42 | # self.a = int(value) 43 | self.a = value 44 | 45 | def __str__(self): 46 | return f"{self.__class__.__name__}(a={self.a})" 47 | 48 | 49 | shape = Shape() 50 | print(shape) 51 | shape.get_area() 52 | 53 | rectangle_1 = Rectangle(2, 6) 54 | print(rectangle_1) 55 | rectangle_2 = Rectangle(3, 5) 56 | print(rectangle_2) 57 | 58 | print("r1 area:", rectangle_1.get_area()) 59 | print("r2 area:", rectangle_2.get_area()) 60 | 61 | sq1 = Square(6) 62 | print(sq1) 63 | print(sq1.a) 64 | print(sq1.b) 65 | print("sq1 square", sq1.get_area()) 66 | 67 | sq1.b = 7 68 | print(sq1) 69 | print(sq1.get_area()) 70 | 71 | sq1.a = 9 72 | print(sq1) 73 | print(sq1.get_area()) 74 | -------------------------------------------------------------------------------- /open-lessons/oop.25.08.2022/step_1.py: -------------------------------------------------------------------------------- 1 | a = 1 2 | b = "2" 3 | 4 | c = a + b 5 | 6 | print("c", c) 7 | 8 | d = c ** 2 9 | print("d", d) 10 | -------------------------------------------------------------------------------- /open-lessons/oop.25.08.2022/step_2.py: -------------------------------------------------------------------------------- 1 | # 1 / 0 2 | # print("never!") 3 | 4 | # print(ZeroDivisionError, ZeroDivisionError.mro()) 5 | # print(TypeError, TypeError.mro()) 6 | # raise Exception 7 | 8 | def call_exc(): 9 | exc = Exception("unexpected error!") 10 | raise exc 11 | 12 | 13 | def another(): 14 | raise ValueError 15 | call_exc() 16 | 17 | 18 | def main(): 19 | call_exc() 20 | 21 | 22 | if __name__ == '__main__': 23 | another() 24 | main() 25 | print("never!") 26 | -------------------------------------------------------------------------------- /open-lessons/oop.25.08.2022/step_3.py: -------------------------------------------------------------------------------- 1 | class MyError(Exception): 2 | pass 3 | 4 | 5 | print(MyError, MyError.mro()) 6 | 7 | raise MyError("this is an error") 8 | -------------------------------------------------------------------------------- /open-lessons/oop.25.08.2022/step_4.py: -------------------------------------------------------------------------------- 1 | from typing import Union 2 | 3 | 4 | class InvalidOperandError(TypeError): 5 | pass 6 | 7 | 8 | class Point: 9 | def __init__(self, x, y): 10 | self.x = x 11 | self.y = y 12 | 13 | def __str__(self): 14 | return str([self.x, self.y]) 15 | 16 | def __repr__(self): 17 | return f"{self.__class__.__name__}(" \ 18 | f"x={self.x}, y={self.y})" 19 | 20 | # def add(self, other: list | tuple | "Point"): 21 | def add(self, other: Union["Point", list, tuple]): 22 | # if not isinstance(point, self.__class__): 23 | # raise InvalidOperandError( 24 | # f"Should be {self.__class__}, not" 25 | # f" {type(point)}") 26 | if isinstance(other, self.__class__): 27 | return self.__class__( 28 | x=self.x + other.x, 29 | y=self.y + other.y, 30 | ) 31 | 32 | if ( 33 | isinstance(other, (tuple, list)) 34 | and len(other) == 2 35 | ): 36 | x, y = other 37 | return self.__class__( 38 | x=self.x + x, 39 | y=self.y + y, 40 | ) 41 | 42 | raise InvalidOperandError( 43 | f"Should be {self.__class__}, not" 44 | f" {type(other)}") 45 | 46 | def __add__(self, other): 47 | return self.add(other) 48 | 49 | # def __radd__(self, other: "Point"): 50 | def __radd__(self, other): 51 | return self.add(other) 52 | 53 | def __iadd__(self, point: "Point"): 54 | if not isinstance(point, self.__class__): 55 | raise InvalidOperandError( 56 | f"Should be {self.__class__}, not" 57 | f" {type(point)}") 58 | self.x += point.x 59 | self.y += point.y 60 | return self 61 | 62 | 63 | p1 = Point(1, 2) 64 | p2 = Point(x=3, y=4) 65 | 66 | print(p1) 67 | print(p2) 68 | print([p1, p2]) 69 | 70 | p3 = p1 + p2 71 | print(p3) 72 | 73 | p4 = p3.add(p2) 74 | print(p4) 75 | 76 | res = (5, 7) + p1 77 | print(res) 78 | 79 | print([res, p1 + [1, 2]]) 80 | 81 | list1 = [1, 2, 3] 82 | list2 = [4, 5, 6] 83 | print("id l1", id(list1)) 84 | 85 | list3 = list1 + list2 86 | print(list1) 87 | print("id l1", id(list1)) 88 | 89 | print(list2) 90 | print(list3) 91 | 92 | list1 += list2 93 | print(list1) 94 | print("id l1", id(list1)) 95 | print(list2) 96 | 97 | list1.append(7) 98 | print(list1) 99 | print("id l1", id(list1)) 100 | 101 | 102 | print([p1, p2]) 103 | print("id p1", id(p1)) 104 | p1 += p2 105 | print("id p1", id(p1)) 106 | print([p1, p2]) 107 | 108 | -------------------------------------------------------------------------------- /open-lessons/oop.25.08.2022/step_5.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | def get_weather_conditions(city: str): 4 | if city.lower() != "moscow": 5 | raise ValueError("invalid city") 6 | 7 | return { 8 | # "temp": 23, 9 | # "humidity": 50, 10 | "rain_chance": 60, 11 | } 12 | 13 | 14 | def rain_tomorrow(city) -> bool | None: 15 | print("Will it rain tomorrow?") 16 | # return True 17 | # return False 18 | try: 19 | conditions = get_weather_conditions(city) 20 | except ValueError: 21 | return None 22 | # return 23 | 24 | if "rain_chance" not in conditions: 25 | return None 26 | 27 | return conditions["rain_chance"] > 50 28 | 29 | # if (rain_chance := conditions.get("rain_chance")) is None: 30 | # return None 31 | # return rain_chance > 50 32 | 33 | 34 | res = rain_tomorrow("Omsk") 35 | # res = get_weather_conditions("Omsk") 36 | print("res for Omsk:", res) 37 | 38 | res = rain_tomorrow("Sochi") 39 | print("res for Sochi:", res) 40 | 41 | res = rain_tomorrow("Moscow") 42 | print("res for Moscow:", res) 43 | -------------------------------------------------------------------------------- /open-lessons/oop.25.08.2022/step_6.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | logger = logging.getLogger(__name__) 4 | 5 | 6 | def div_safe(a, b): 7 | try: 8 | c = a / b 9 | # except ArithmeticError: 10 | # return 1 11 | except ZeroDivisionError: 12 | # logger.exception("divide by zero") 13 | # logger.error("divide by zero") 14 | # logger.warning("divide by zero", exc_info=True) 15 | 16 | print("please don't divide by zero") 17 | return 42 18 | except TypeError as e: 19 | # print(e.__traceback__) 20 | # logger.warning("type error", exc_info=e) 21 | 22 | print("oops", e.args, repr(e)) 23 | # return None 24 | # except Exception: 25 | # pass 26 | else: 27 | print("ok div, res:", [a, b], c) 28 | return c 29 | finally: 30 | print("finally for", [a, b]) 31 | 32 | print("leaving func, args:", [a, b]) 33 | 34 | 35 | 36 | print(div_safe(1, 0)) 37 | print(div_safe(2, 1)) 38 | print(div_safe(10, 2)) 39 | print(div_safe("10", 2)) 40 | -------------------------------------------------------------------------------- /open-lessons/oop.base.11.06.2020/multi_inheritance.py: -------------------------------------------------------------------------------- 1 | class A: 2 | def a(self): 3 | return "A" 4 | 5 | 6 | class B: 7 | def b(self): 8 | return "B" 9 | 10 | 11 | class C: 12 | def c(self): 13 | return "C" 14 | 15 | 16 | class MyMixin(): 17 | def __init__(self): 18 | self.__value = 40 19 | self.__value_square = 0 20 | self.update_square() 21 | 22 | def update_square(self): 23 | self.__value_square = self.value ** 2 24 | 25 | @property 26 | def value_square(self): 27 | return self.__value_square 28 | 29 | @property 30 | def value(self): 31 | return self.__value 32 | 33 | @value.setter 34 | def value(self, v): 35 | if v < 0: 36 | raise Exception("Value has to be 0 or greater") 37 | self.__value = v 38 | self.update_square() 39 | 40 | 41 | class MyClass(A, B, C, MyMixin): 42 | def method(self): 43 | return " ".join((self.a(), self.b(), self.c())) 44 | 45 | 46 | if __name__ == '__main__': 47 | print(MyClass.__mro__) 48 | my_cls = MyClass() 49 | print(my_cls.method()) 50 | 51 | print("value", my_cls.value) 52 | print("square", my_cls.value_square) 53 | print("set value to 30") 54 | my_cls.value = 30 55 | print("now value", my_cls.value) 56 | print("now value square", my_cls.value_square) 57 | -------------------------------------------------------------------------------- /open-lessons/oop.base.11.06.2020/people.py: -------------------------------------------------------------------------------- 1 | class Person: 2 | WHAT_TO_SAY_PERSON = "My name is {name} and I'm {age} years old" 3 | 4 | def __init__(self, name: str, age: int): 5 | self.name = name 6 | self.age = age 7 | 8 | def say(self): 9 | speech = self.WHAT_TO_SAY_PERSON.format(name=self.name, age=self.age) 10 | print(speech) 11 | return speech 12 | 13 | 14 | class Employee(Person): 15 | WHAT_TO_SAY_EMPLOYEE = "I work {years} years already and get {salary} annually" 16 | 17 | def __init__(self, name: str, age: int, salary: int, experience: int): 18 | super().__init__(name, age) 19 | self.salary = salary 20 | self.experience = experience 21 | 22 | def say(self): 23 | initial_speech = super().say() 24 | speech = "\n".join(( 25 | initial_speech, 26 | self.WHAT_TO_SAY_EMPLOYEE.format(years=self.experience, salary=self.salary) 27 | )) 28 | print(speech) 29 | return speech 30 | 31 | 32 | if __name__ == '__main__': 33 | employee = Employee("John", 30, 100_000, 7) 34 | employee.say() 35 | -------------------------------------------------------------------------------- /open-lessons/oop.base.11.06.2020/shapes.py: -------------------------------------------------------------------------------- 1 | class BaseShape: 2 | def area(self): 3 | raise NotImplementedError 4 | 5 | 6 | class Rectangle(BaseShape): 7 | def __init__(self, length: int, width: int, **kwargs): 8 | self.length = length 9 | self.width = width 10 | super().__init__(**kwargs) 11 | 12 | def area(self) -> int: 13 | return self.length * self.width 14 | 15 | def perimeter(self) -> int: 16 | return 2 * self.length + 2 * self.width 17 | 18 | 19 | class Square(Rectangle): 20 | def __init__(self, length: int, **kwargs): 21 | super().__init__(length, length, **kwargs) 22 | 23 | 24 | class Triangle(BaseShape): 25 | def __init__(self, base: int, height: int, **kwargs): 26 | self.base = base 27 | self.height = height 28 | super().__init__(**kwargs) 29 | 30 | def tri_area(self) -> float: 31 | return 0.5 * self.base * self.height 32 | 33 | 34 | class RightPyramid(Square, Triangle): 35 | def __init__(self, base: int, slant_height: int): 36 | super().__init__(base=base, height=slant_height, length=base) 37 | self.base = base 38 | self.slant_height = slant_height 39 | 40 | def pyramid_area(self): 41 | base_area = self.area() 42 | side_area = self.tri_area() 43 | return side_area * 4 + base_area 44 | 45 | 46 | if __name__ == '__main__': 47 | rectangle = Rectangle(2, 4) 48 | print("Rectangle:", rectangle.length, rectangle.width) 49 | print("Area:", rectangle.area()) 50 | print("Perimeter:", rectangle.perimeter()) 51 | 52 | square = Square(5) 53 | print("Square:", square.length) 54 | print("Area:", square.area()) 55 | print("Perimeter:", square.perimeter()) 56 | 57 | triangle = Triangle(4, 6) 58 | print("triangle area:", triangle.tri_area()) 59 | 60 | pyramid = RightPyramid(10, 12) 61 | print(RightPyramid.__mro__) 62 | print("pyramid area", pyramid.pyramid_area()) 63 | -------------------------------------------------------------------------------- /open-lessons/oop.base.11.06.2020/vehicles.py: -------------------------------------------------------------------------------- 1 | class BaseVehicle: 2 | def make_sound(self): 3 | raise NotImplementedError 4 | 5 | 6 | class Car(BaseVehicle): 7 | SOUND = "beep!" 8 | CONSUMPTION = 10 9 | 10 | def __init__(self, fuel: int, doors: int = 5, wheels: int = 4): 11 | """ 12 | :param fuel: 13 | :param doors: 14 | :param wheels: 15 | """ 16 | self.__fuel = fuel 17 | self.__doors = doors 18 | self.__wheels = wheels 19 | 20 | def __str__(self): 21 | return f"{self.__class__.__name__}. fuel: {self.__fuel}, doors: {self.__doors}, wheels: {self.__wheels}" 22 | 23 | @property 24 | def fuel(self): 25 | return self.__fuel 26 | 27 | def make_sound(self): 28 | print(self.SOUND) 29 | return self.SOUND 30 | 31 | def drive(self, distance: int): 32 | to_spend = self.CONSUMPTION * distance 33 | if self.__fuel < to_spend: 34 | raise Exception("Not enough fuel!") 35 | self.__fuel -= to_spend 36 | print(f"Drive {distance}, spend: {to_spend}, fuel left: {self.__fuel}") 37 | return self.__fuel 38 | 39 | def add_fuel(self, amount: int): 40 | self.__fuel += amount 41 | print(f"Added {amount}, left {self.__fuel}") 42 | return self.__fuel 43 | 44 | 45 | class SportCar(Car): 46 | SOUND = "beep beep!!" 47 | CONSUMPTION = 30 48 | 49 | def __init__( 50 | self, 51 | fuel: int, 52 | doors: int = 2, 53 | wheels: int = 4, 54 | doors_open_upwards: bool = True, 55 | ): 56 | super().__init__(fuel, doors, wheels) 57 | self.doors_open_upwards = doors_open_upwards 58 | 59 | 60 | def drive_by_car(car: Car, distance: int): 61 | print("Going", distance, "by", car) 62 | car.drive(distance) 63 | print("fuel left", car._Car__fuel) 64 | 65 | 66 | if __name__ == '__main__': 67 | car = Car(1000) 68 | print("car:", car) 69 | # car._Car__fuel = 100 70 | # print("car fuel:", car.fuel) 71 | print("Drive, fuel left:", car.drive(15)) 72 | print(car) 73 | print("Fuel left:", car.add_fuel(100)) 74 | 75 | # car.drive(30) 76 | # car.drive(30) 77 | # car.drive(30) 78 | 79 | sport_car = SportCar(500) 80 | 81 | print("car sound:") 82 | car.make_sound() 83 | print("sport car sound:") 84 | sport_car.make_sound() 85 | 86 | sport_car.drive(10) 87 | sport_car._Car__fuel = 300 88 | sport_car.drive(10) 89 | 90 | car.add_fuel(300) 91 | drive_by_car(car, 70) 92 | sport_car.add_fuel(1000) 93 | drive_by_car(sport_car, 30) 94 | -------------------------------------------------------------------------------- /open-lessons/pb-and-grpc.07.11.2024/README.md: -------------------------------------------------------------------------------- 1 | # Знакомство с Protobuf и gRPC. Работа с gRPC на Python 2 | 3 | 4 | ## Ссылки 5 | - https://grpc.github.io/grpc/core/md_doc_g_stands_for.html 6 | - https://grpc.io/docs/languages/python/quickstart/ 7 | - https://github.com/protocolbuffers/protobuf/releases/tag/v26.0 8 | - https://protobuf.dev/getting-started/pythontutorial/ 9 | - https://github.com/grpc/grpc/tree/master/examples/python 10 | 11 | ## Шаги 12 | 13 | - настроили Poetry 14 | - установили Python пакет `protobuf` 15 | - поставили protobuf на macOS командой 16 | ```shell 17 | brew install protobuf 18 | ``` 19 | - компилируем протокол на Python 20 | ```shell 21 | protoc --proto_path=protos --python_out=./pb protos/hello.proto 22 | ``` 23 | - компилируем протокол в Python плюс аннотации типов 24 | ```shell 25 | protoc --proto_path=protos --python_out=./pb --pyi_out=./pb protos/hello.proto 26 | ``` 27 | - установили Python пакет `grpcio` 28 | - установили Python пакет `grpcio-tools` 29 | - описали сервис `Greeter` 30 | - компилируем командой 31 | ```shell 32 | python -m grpc_tools.protoc --proto_path=protos --python_out=./pb --pyi_out=./pb --grpc_python_out=./pb protos/hello.proto 33 | ``` 34 | - правим импорт 35 | - реализуем сервисер 36 | - запускаем сервер 37 | - вызываем клиент 38 | 39 | 40 | Example stream: 41 | ```python 42 | # client 43 | def request_iter(): 44 | for idx in range(5): 45 | yield service_pb2.HelloRequest(name=f"Stream #{idx}") 46 | 47 | 48 | stub.GreetStream(request_iter()) 49 | 50 | # server 51 | for r in request_iter: 52 | print(f"Stream request {r}") 53 | yield service_pb2.HelloReply(message=f"Hello, {r.name}!") 54 | ``` 55 | -------------------------------------------------------------------------------- /open-lessons/pb-and-grpc.07.11.2024/address-book.txt: -------------------------------------------------------------------------------- 1 | *John Doejdoe@example.com" 2 | 3 | 555-4321" 4 | 123-4321 -------------------------------------------------------------------------------- /open-lessons/pb-and-grpc.07.11.2024/client_hello.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from concurrent import futures 3 | from random import randint 4 | 5 | import grpc 6 | 7 | from common import configure_logging 8 | from pb import hello_pb2, hello_pb2_grpc 9 | 10 | log = logging.getLogger(__name__) 11 | 12 | 13 | def run() -> None: 14 | log.info("Start greeting") 15 | with grpc.insecure_channel('localhost:50051') as channel: 16 | stub = hello_pb2_grpc.GreeterStub(channel) 17 | 18 | hello_request = hello_pb2.HelloRequest(name="Sam") 19 | hello_reply: hello_pb2.HelloReply = ( 20 | stub.Greet(hello_request) 21 | ) 22 | log.info( 23 | "Reply for %s: %s. Text: %r, number: %s", 24 | hello_request.name, 25 | hello_reply, 26 | hello_reply.hello.text, 27 | hello_reply.hello.number, 28 | ) 29 | 30 | log.info("Finished") 31 | 32 | 33 | def main() -> None: 34 | configure_logging() 35 | run() 36 | 37 | 38 | if __name__ == '__main__': 39 | main() 40 | 41 | -------------------------------------------------------------------------------- /open-lessons/pb-and-grpc.07.11.2024/common.py: -------------------------------------------------------------------------------- 1 | __all__ = ( 2 | "DEFAULT_LOG_FORMAT", 3 | "configure_logging", 4 | ) 5 | 6 | import logging 7 | 8 | DEFAULT_LOG_FORMAT = ( 9 | # "[%(asctime)s.%(msecs)03d] %(module)10s:%(lineno)-3d %(levelname)-7s - %(message)s" 10 | "%(module)10s:%(lineno)-3d %(levelname)-7s - %(message)s" 11 | ) 12 | 13 | 14 | def configure_logging(level: int = logging.INFO) -> None: 15 | logging.basicConfig( 16 | level=level, 17 | datefmt="%Y-%m-%d %H:%M:%S", 18 | format=DEFAULT_LOG_FORMAT, 19 | ) 20 | -------------------------------------------------------------------------------- /open-lessons/pb-and-grpc.07.11.2024/demo_pb_address_book.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | from pb import address_book_pb2 4 | 5 | 6 | ADDRESS_BOOK_FILE = Path(__file__).parent / "address-book.txt" 7 | 8 | 9 | def write() -> None: 10 | person = address_book_pb2.Person() 11 | person.id = 42 12 | person.name = "John Doe" 13 | person.email = "jdoe@example.com" 14 | 15 | phone_home = person.phones.add() 16 | phone_home.number = "555-4321" 17 | 18 | phone_mobile = person.phones.add() 19 | phone_mobile.number = "123-4321" 20 | phone_mobile.type = address_book_pb2.Person.PHONE_TYPE_MOBILE 21 | 22 | print(person) 23 | print(person.SerializeToString()) 24 | with ADDRESS_BOOK_FILE.open("wb") as f: 25 | f.write(person.SerializeToString()) 26 | 27 | 28 | def read() -> None: 29 | person = address_book_pb2.Person() 30 | print("person is initialized (before):", person.IsInitialized()) 31 | with ADDRESS_BOOK_FILE.open("rb") as f: 32 | person.ParseFromString(f.read()) 33 | print("person is initialized (after):", person.IsInitialized()) 34 | print(person) 35 | 36 | 37 | def main() -> None: 38 | # write() 39 | read() 40 | 41 | 42 | if __name__ == "__main__": 43 | main() 44 | 45 | -------------------------------------------------------------------------------- /open-lessons/pb-and-grpc.07.11.2024/demo_pb_hello.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | from pb import hello_pb2 4 | 5 | 6 | HELLO_FILE = Path(__file__).parent / "hello.txt" 7 | 8 | 9 | def write() -> None: 10 | # hello = hello_pb2.Hello() 11 | # hello.text = "Hello World!" 12 | hello = hello_pb2.Hello( 13 | text="Hello world!", 14 | ) 15 | print(hello) 16 | print(hello.SerializeToString()) 17 | with HELLO_FILE.open("wb") as f: 18 | f.write(hello.SerializeToString()) 19 | 20 | 21 | def read() -> None: 22 | hello = hello_pb2.Hello() 23 | print("hello is initialized (before):", hello.IsInitialized()) 24 | with HELLO_FILE.open("rb") as f: 25 | hello.ParseFromString(f.read()) 26 | print("hello is initialized (after):", hello.IsInitialized()) 27 | print(hello) 28 | 29 | 30 | def main() -> None: 31 | # write() 32 | read() 33 | 34 | 35 | if __name__ == "__main__": 36 | main() 37 | 38 | -------------------------------------------------------------------------------- /open-lessons/pb-and-grpc.07.11.2024/hello.txt: -------------------------------------------------------------------------------- 1 | 2 | Hello world! -------------------------------------------------------------------------------- /open-lessons/pb-and-grpc.07.11.2024/pb/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OtusTeam/BasePython/d87ee93755f4c2dbb4c2b4bbd35743a2c6e44043/open-lessons/pb-and-grpc.07.11.2024/pb/__init__.py -------------------------------------------------------------------------------- /open-lessons/pb-and-grpc.07.11.2024/pb/address_book_pb2.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by the protocol buffer compiler. DO NOT EDIT! 3 | # NO CHECKED-IN PROTOBUF GENCODE 4 | # source: address-book.proto 5 | # Protobuf Python Version: 5.28.3 6 | """Generated protocol buffer code.""" 7 | from google.protobuf import descriptor as _descriptor 8 | from google.protobuf import descriptor_pool as _descriptor_pool 9 | from google.protobuf import runtime_version as _runtime_version 10 | from google.protobuf import symbol_database as _symbol_database 11 | from google.protobuf.internal import builder as _builder 12 | _runtime_version.ValidateProtobufRuntimeVersion( 13 | _runtime_version.Domain.PUBLIC, 14 | 5, 15 | 28, 16 | 3, 17 | '', 18 | 'address-book.proto' 19 | ) 20 | # @@protoc_insertion_point(imports) 21 | 22 | _sym_db = _symbol_database.Default() 23 | 24 | 25 | 26 | 27 | DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x12\x61\x64\x64ress-book.proto\x12\x0b\x61\x64\x64ressbook\"\xa9\x02\n\x06Person\x12\n\n\x02id\x18\x01 \x01(\x05\x12\x0c\n\x04name\x18\x02 \x01(\t\x12\r\n\x05\x65mail\x18\x03 \x01(\t\x12/\n\x06phones\x18\x04 \x03(\x0b\x32\x1f.addressbook.Person.PhoneNumber\x1a[\n\x0bPhoneNumber\x12\x0e\n\x06number\x18\x01 \x01(\t\x12<\n\x04type\x18\x02 \x01(\x0e\x32\x1d.addressbook.Person.PhoneType:\x0fPHONE_TYPE_HOME\"h\n\tPhoneType\x12\x1a\n\x16PHONE_TYPE_UNSPECIFIED\x10\x00\x12\x15\n\x11PHONE_TYPE_MOBILE\x10\x01\x12\x13\n\x0fPHONE_TYPE_HOME\x10\x02\x12\x13\n\x0fPHONE_TYPE_WORK\x10\x03\"2\n\x0b\x41\x64\x64ressBook\x12#\n\x06people\x18\x01 \x03(\x0b\x32\x13.addressbook.Person') 28 | 29 | _globals = globals() 30 | _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) 31 | _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'address_book_pb2', _globals) 32 | if not _descriptor._USE_C_DESCRIPTORS: 33 | DESCRIPTOR._loaded_options = None 34 | _globals['_PERSON']._serialized_start=36 35 | _globals['_PERSON']._serialized_end=333 36 | _globals['_PERSON_PHONENUMBER']._serialized_start=136 37 | _globals['_PERSON_PHONENUMBER']._serialized_end=227 38 | _globals['_PERSON_PHONETYPE']._serialized_start=229 39 | _globals['_PERSON_PHONETYPE']._serialized_end=333 40 | _globals['_ADDRESSBOOK']._serialized_start=335 41 | _globals['_ADDRESSBOOK']._serialized_end=385 42 | # @@protoc_insertion_point(module_scope) 43 | -------------------------------------------------------------------------------- /open-lessons/pb-and-grpc.07.11.2024/pb/address_book_pb2.pyi: -------------------------------------------------------------------------------- 1 | from google.protobuf.internal import containers as _containers 2 | from google.protobuf.internal import enum_type_wrapper as _enum_type_wrapper 3 | from google.protobuf import descriptor as _descriptor 4 | from google.protobuf import message as _message 5 | from typing import ClassVar as _ClassVar, Iterable as _Iterable, Mapping as _Mapping, Optional as _Optional, Union as _Union 6 | 7 | DESCRIPTOR: _descriptor.FileDescriptor 8 | 9 | class Person(_message.Message): 10 | __slots__ = ("id", "name", "email", "phones") 11 | class PhoneType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): 12 | __slots__ = () 13 | PHONE_TYPE_UNSPECIFIED: _ClassVar[Person.PhoneType] 14 | PHONE_TYPE_MOBILE: _ClassVar[Person.PhoneType] 15 | PHONE_TYPE_HOME: _ClassVar[Person.PhoneType] 16 | PHONE_TYPE_WORK: _ClassVar[Person.PhoneType] 17 | PHONE_TYPE_UNSPECIFIED: Person.PhoneType 18 | PHONE_TYPE_MOBILE: Person.PhoneType 19 | PHONE_TYPE_HOME: Person.PhoneType 20 | PHONE_TYPE_WORK: Person.PhoneType 21 | class PhoneNumber(_message.Message): 22 | __slots__ = ("number", "type") 23 | NUMBER_FIELD_NUMBER: _ClassVar[int] 24 | TYPE_FIELD_NUMBER: _ClassVar[int] 25 | number: str 26 | type: Person.PhoneType 27 | def __init__(self, number: _Optional[str] = ..., type: _Optional[_Union[Person.PhoneType, str]] = ...) -> None: ... 28 | ID_FIELD_NUMBER: _ClassVar[int] 29 | NAME_FIELD_NUMBER: _ClassVar[int] 30 | EMAIL_FIELD_NUMBER: _ClassVar[int] 31 | PHONES_FIELD_NUMBER: _ClassVar[int] 32 | id: int 33 | name: str 34 | email: str 35 | phones: _containers.RepeatedCompositeFieldContainer[Person.PhoneNumber] 36 | def __init__(self, id: _Optional[int] = ..., name: _Optional[str] = ..., email: _Optional[str] = ..., phones: _Optional[_Iterable[_Union[Person.PhoneNumber, _Mapping]]] = ...) -> None: ... 37 | 38 | class AddressBook(_message.Message): 39 | __slots__ = ("people",) 40 | PEOPLE_FIELD_NUMBER: _ClassVar[int] 41 | people: _containers.RepeatedCompositeFieldContainer[Person] 42 | def __init__(self, people: _Optional[_Iterable[_Union[Person, _Mapping]]] = ...) -> None: ... 43 | -------------------------------------------------------------------------------- /open-lessons/pb-and-grpc.07.11.2024/pb/hello_pb2.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by the protocol buffer compiler. DO NOT EDIT! 3 | # NO CHECKED-IN PROTOBUF GENCODE 4 | # source: hello.proto 5 | # Protobuf Python Version: 5.27.2 6 | """Generated protocol buffer code.""" 7 | from google.protobuf import descriptor as _descriptor 8 | from google.protobuf import descriptor_pool as _descriptor_pool 9 | from google.protobuf import runtime_version as _runtime_version 10 | from google.protobuf import symbol_database as _symbol_database 11 | from google.protobuf.internal import builder as _builder 12 | _runtime_version.ValidateProtobufRuntimeVersion( 13 | _runtime_version.Domain.PUBLIC, 14 | 5, 15 | 27, 16 | 2, 17 | '', 18 | 'hello.proto' 19 | ) 20 | # @@protoc_insertion_point(imports) 21 | 22 | _sym_db = _symbol_database.Default() 23 | 24 | 25 | 26 | 27 | DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0bhello.proto\x12\x05hello\"%\n\x05Hello\x12\x0c\n\x04text\x18\x01 \x02(\t\x12\x0e\n\x06number\x18\x02 \x01(\x03\"\x1c\n\x0cHelloRequest\x12\x0c\n\x04name\x18\x01 \x02(\t\")\n\nHelloReply\x12\x1b\n\x05hello\x18\x01 \x02(\x0b\x32\x0c.hello.Hello2<\n\x07Greeter\x12\x31\n\x05Greet\x12\x13.hello.HelloRequest\x1a\x11.hello.HelloReply\"\x00') 28 | 29 | _globals = globals() 30 | _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) 31 | _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'hello_pb2', _globals) 32 | if not _descriptor._USE_C_DESCRIPTORS: 33 | DESCRIPTOR._loaded_options = None 34 | _globals['_HELLO']._serialized_start=22 35 | _globals['_HELLO']._serialized_end=59 36 | _globals['_HELLOREQUEST']._serialized_start=61 37 | _globals['_HELLOREQUEST']._serialized_end=89 38 | _globals['_HELLOREPLY']._serialized_start=91 39 | _globals['_HELLOREPLY']._serialized_end=132 40 | _globals['_GREETER']._serialized_start=134 41 | _globals['_GREETER']._serialized_end=194 42 | # @@protoc_insertion_point(module_scope) 43 | -------------------------------------------------------------------------------- /open-lessons/pb-and-grpc.07.11.2024/pb/hello_pb2.pyi: -------------------------------------------------------------------------------- 1 | from google.protobuf import descriptor as _descriptor 2 | from google.protobuf import message as _message 3 | from typing import ClassVar as _ClassVar, Mapping as _Mapping, Optional as _Optional, Union as _Union 4 | 5 | DESCRIPTOR: _descriptor.FileDescriptor 6 | 7 | class Hello(_message.Message): 8 | __slots__ = ("text", "number") 9 | TEXT_FIELD_NUMBER: _ClassVar[int] 10 | NUMBER_FIELD_NUMBER: _ClassVar[int] 11 | text: str 12 | number: int 13 | def __init__(self, text: _Optional[str] = ..., number: _Optional[int] = ...) -> None: ... 14 | 15 | class HelloRequest(_message.Message): 16 | __slots__ = ("name",) 17 | NAME_FIELD_NUMBER: _ClassVar[int] 18 | name: str 19 | def __init__(self, name: _Optional[str] = ...) -> None: ... 20 | 21 | class HelloReply(_message.Message): 22 | __slots__ = ("hello",) 23 | HELLO_FIELD_NUMBER: _ClassVar[int] 24 | hello: Hello 25 | def __init__(self, hello: _Optional[_Union[Hello, _Mapping]] = ...) -> None: ... 26 | -------------------------------------------------------------------------------- /open-lessons/pb-and-grpc.07.11.2024/pb/hello_pb2_grpc.py: -------------------------------------------------------------------------------- 1 | # Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! 2 | """Client and server classes corresponding to protobuf-defined services.""" 3 | import grpc 4 | import warnings 5 | 6 | from pb import hello_pb2 as hello__pb2 7 | 8 | GRPC_GENERATED_VERSION = '1.67.1' 9 | GRPC_VERSION = grpc.__version__ 10 | _version_not_supported = False 11 | 12 | try: 13 | from grpc._utilities import first_version_is_lower 14 | _version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION) 15 | except ImportError: 16 | _version_not_supported = True 17 | 18 | if _version_not_supported: 19 | raise RuntimeError( 20 | f'The grpc package installed is at version {GRPC_VERSION},' 21 | + f' but the generated code in hello_pb2_grpc.py depends on' 22 | + f' grpcio>={GRPC_GENERATED_VERSION}.' 23 | + f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}' 24 | + f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.' 25 | ) 26 | 27 | 28 | class GreeterStub(object): 29 | """The greeting service definition. 30 | """ 31 | 32 | def __init__(self, channel): 33 | """Constructor. 34 | 35 | Args: 36 | channel: A grpc.Channel. 37 | """ 38 | self.Greet = channel.unary_unary( 39 | '/hello.Greeter/Greet', 40 | request_serializer=hello__pb2.HelloRequest.SerializeToString, 41 | response_deserializer=hello__pb2.HelloReply.FromString, 42 | _registered_method=True) 43 | 44 | 45 | class GreeterServicer(object): 46 | """The greeting service definition. 47 | """ 48 | 49 | def Greet(self, request, context): 50 | """Missing associated documentation comment in .proto file.""" 51 | context.set_code(grpc.StatusCode.UNIMPLEMENTED) 52 | context.set_details('Method not implemented!') 53 | raise NotImplementedError('Method not implemented!') 54 | 55 | 56 | def add_GreeterServicer_to_server(servicer, server): 57 | rpc_method_handlers = { 58 | 'Greet': grpc.unary_unary_rpc_method_handler( 59 | servicer.Greet, 60 | request_deserializer=hello__pb2.HelloRequest.FromString, 61 | response_serializer=hello__pb2.HelloReply.SerializeToString, 62 | ), 63 | } 64 | generic_handler = grpc.method_handlers_generic_handler( 65 | 'hello.Greeter', rpc_method_handlers) 66 | server.add_generic_rpc_handlers((generic_handler,)) 67 | server.add_registered_method_handlers('hello.Greeter', rpc_method_handlers) 68 | 69 | 70 | # This class is part of an EXPERIMENTAL API. 71 | class Greeter(object): 72 | """The greeting service definition. 73 | """ 74 | 75 | @staticmethod 76 | def Greet(request, 77 | target, 78 | options=(), 79 | channel_credentials=None, 80 | call_credentials=None, 81 | insecure=False, 82 | compression=None, 83 | wait_for_ready=None, 84 | timeout=None, 85 | metadata=None): 86 | return grpc.experimental.unary_unary( 87 | request, 88 | target, 89 | '/hello.Greeter/Greet', 90 | hello__pb2.HelloRequest.SerializeToString, 91 | hello__pb2.HelloReply.FromString, 92 | options, 93 | channel_credentials, 94 | insecure, 95 | call_credentials, 96 | compression, 97 | wait_for_ready, 98 | timeout, 99 | metadata, 100 | _registered_method=True) 101 | -------------------------------------------------------------------------------- /open-lessons/pb-and-grpc.07.11.2024/protos/address-book.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | package addressbook; 4 | 5 | message Person { 6 | optional int32 id = 1; 7 | optional string name = 2; 8 | optional string email = 3; 9 | 10 | enum PhoneType { 11 | PHONE_TYPE_UNSPECIFIED = 0; 12 | PHONE_TYPE_MOBILE = 1; 13 | PHONE_TYPE_HOME = 2; 14 | PHONE_TYPE_WORK = 3; 15 | } 16 | 17 | message PhoneNumber { 18 | optional string number = 1; 19 | optional PhoneType type = 2 [default = PHONE_TYPE_HOME]; 20 | } 21 | 22 | repeated PhoneNumber phones = 4; 23 | } 24 | 25 | message AddressBook { 26 | repeated Person people = 1; 27 | } 28 | -------------------------------------------------------------------------------- /open-lessons/pb-and-grpc.07.11.2024/protos/hello.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | package hello; 4 | 5 | 6 | // The greeting service definition. 7 | service Greeter { 8 | rpc Greet (HelloRequest) returns (HelloReply) {} 9 | // rpc GreetStream (stream HelloRequest) returns (stream HelloReply) {} 10 | } 11 | 12 | 13 | message Hello { 14 | required string text = 1; 15 | optional int64 number = 2; 16 | } 17 | 18 | message HelloRequest { 19 | required string name = 1; 20 | } 21 | 22 | message HelloReply { 23 | required Hello hello = 1; 24 | } 25 | -------------------------------------------------------------------------------- /open-lessons/pb-and-grpc.07.11.2024/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | package-mode = false 3 | 4 | [tool.poetry.dependencies] 5 | python = "^3.12" 6 | protobuf = "^5.28.3" 7 | grpcio = "^1.67.1" 8 | 9 | [tool.poetry.group.dev.dependencies] 10 | grpcio-tools = "^1.67.1" 11 | 12 | -------------------------------------------------------------------------------- /open-lessons/pb-and-grpc.07.11.2024/server_hello.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from concurrent import futures 3 | from random import randint 4 | 5 | import grpc 6 | 7 | from common import configure_logging 8 | from pb import hello_pb2, hello_pb2_grpc 9 | 10 | log = logging.getLogger(__name__) 11 | 12 | 13 | class GreeterServicer(hello_pb2_grpc.GreeterServicer): 14 | 15 | def Greet( 16 | self, 17 | request: hello_pb2.HelloRequest, 18 | context: grpc.ServicerContext, 19 | ) -> hello_pb2.HelloReply: 20 | log.info( 21 | "Received hello request for name %r", 22 | request.name, 23 | ) 24 | hello = hello_pb2.Hello( 25 | text=f"Hello, {request.name}!", 26 | number=randint(1, 100), 27 | ) 28 | reply = hello_pb2.HelloReply(hello=hello) 29 | log.info("Respond with %r", reply) 30 | return reply 31 | 32 | 33 | def serve() -> None: 34 | server = grpc.server(futures.ThreadPoolExecutor(max_workers=10)) 35 | hello_pb2_grpc.add_GreeterServicer_to_server( 36 | GreeterServicer(), 37 | server, 38 | ) 39 | server.add_insecure_port('[::]:50051') 40 | server.start() 41 | log.warning("Listening on port 50051") 42 | server.wait_for_termination() 43 | 44 | 45 | def main() -> None: 46 | configure_logging() 47 | try: 48 | serve() 49 | except KeyboardInterrupt: 50 | log.warning('Received keyboard interrupt.') 51 | 52 | 53 | if __name__ == '__main__': 54 | main() 55 | 56 | -------------------------------------------------------------------------------- /open-lessons/pytest.18.11.2020/api.py: -------------------------------------------------------------------------------- 1 | API_URL = "weather.com" 2 | 3 | 4 | class WeatherApi: 5 | def __init__(self, base_url: str): 6 | self.base_url = base_url 7 | 8 | def fetch_weather(self, city: str) -> dict: 9 | print("got in fetch_weather") 10 | import requests 11 | response = requests.get(url=self.base_url, params={"city": city}) 12 | return response.json() 13 | 14 | 15 | class Weather: 16 | def __init__(self, api: WeatherApi): 17 | self.api = api 18 | 19 | def get_temperature(self, city: str) -> int: 20 | res: dict = self.api.fetch_weather(city=city) 21 | temperature = res["temperature"] 22 | return temperature 23 | 24 | def get_humidity(self, city: str) -> float: 25 | res: dict = self.api.fetch_weather(city=city) 26 | humidity = res["humidity"] 27 | return humidity 28 | -------------------------------------------------------------------------------- /open-lessons/pytest.18.11.2020/solver.py: -------------------------------------------------------------------------------- 1 | 2 | class Solver: 3 | 4 | @classmethod 5 | def add(cls, a, b): 6 | res = a + b 7 | print("result a + b", res) 8 | return res 9 | 10 | 11 | def mul(a, b): 12 | return a * b 13 | 14 | 15 | SUB_ERROR_TEXT = "All arguments have to be of type int or float" 16 | 17 | 18 | def sub(a, b): 19 | if not all( 20 | map( 21 | lambda x: isinstance(x, (int, float)), 22 | (a, b), 23 | ) 24 | ): 25 | raise TypeError(SUB_ERROR_TEXT) 26 | return a - b 27 | 28 | 29 | if __name__ == '__main__': 30 | res = Solver.add(1, 2) 31 | print("expected 3, got", res) 32 | -------------------------------------------------------------------------------- /open-lessons/pytest.18.11.2020/test_api.py: -------------------------------------------------------------------------------- 1 | from unittest import mock 2 | 3 | import pytest 4 | from api import API_URL, WeatherApi, Weather 5 | 6 | 7 | def get_prepared_weather_helper(): 8 | api = WeatherApi(API_URL) 9 | weather = Weather(api=api) 10 | return weather 11 | 12 | 13 | @pytest.fixture 14 | def mocked_fetch_weather(): 15 | with mock.patch.object(WeatherApi, 'fetch_weather', autospec=True) as mocked_fetch_weather: 16 | # print('mocked fetch_weather', WeatherApi.fetch_weather) 17 | yield mocked_fetch_weather 18 | # print('teardown api') 19 | # api.close() 20 | # print('not mocked fetch_weather', api.fetch_weather) 21 | # print('done teardown for api') 22 | 23 | 24 | @pytest.fixture 25 | def weather_api(mocked_fetch_weather): 26 | api = WeatherApi(API_URL) 27 | return api 28 | 29 | 30 | @pytest.fixture 31 | def weather(weather_api): 32 | weather = Weather(api=weather_api) 33 | return weather 34 | 35 | 36 | @pytest.fixture(params=["Moscow", "NYC", "Voronezh"]) 37 | def city(request): 38 | return request.param 39 | 40 | 41 | class TestWeatherApi: 42 | def test_fetch_weather(self, weather_api, city): 43 | res = weather_api.fetch_weather(city=city) 44 | assert res is not None 45 | 46 | 47 | class TestWeather: 48 | 49 | def test_get_temperature(self, mocked_fetch_weather, weather, city): 50 | # weather = get_prepared_weather_helper() 51 | temperature = 13 52 | mocked_fetch_weather.return_value = {"temperature": temperature} 53 | res = weather.get_temperature(city) 54 | assert isinstance(res, int) 55 | assert res == temperature 56 | # weather_api.fetch_weather.assert_called() 57 | # weather_api.fetch_weather.assert_called_once() 58 | # call_result = weather_api.fetch_weather.asert_called_once_with(city=city) 59 | call_result = mocked_fetch_weather.assert_called_once_with(weather.api, city=city) 60 | assert call_result is None 61 | 62 | def test_get_humidity(self, mocked_fetch_weather, weather, city): 63 | humidity = 42.5 64 | mocked_fetch_weather.return_value = {"humidity": humidity} 65 | res = weather.get_humidity(city) 66 | assert isinstance(res, float) 67 | assert res == humidity 68 | # weather_api.fetch_weather.assert_called() 69 | # weather_api.fetch_weather.assert_called_once() 70 | mocked_fetch_weather.assert_called_once_with(weather.api, city=city) 71 | -------------------------------------------------------------------------------- /open-lessons/pytest.18.11.2020/test_solver.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | 3 | import pytest 4 | 5 | from solver import Solver, mul, sub, SUB_ERROR_TEXT 6 | 7 | 8 | class TestSolver(TestCase): 9 | 10 | def test_add(self): 11 | res = Solver.add(1, 2) 12 | self.assertEqual(res, 3) 13 | 14 | 15 | @pytest.mark.parametrize('args, expected_result', [ 16 | (('spam', 3), 'spamspamspam'), 17 | ((4, 3), 12), 18 | ]) 19 | def test_mul(args, expected_result): 20 | res = mul(*args) 21 | assert res == expected_result 22 | 23 | 24 | class TestSub: 25 | def test_sub_raises(self): 26 | with pytest.raises(TypeError) as exc_info: 27 | sub('', 1) 28 | 29 | assert str(exc_info.value) == SUB_ERROR_TEXT 30 | 31 | @pytest.mark.parametrize('args, expected_result', [ 32 | pytest.param( 33 | (3, 4), -1, 34 | id="sub_int", 35 | ), 36 | pytest.param( 37 | (5.6, .5), 5.1, 38 | id="sub_float", 39 | ), 40 | ]) 41 | def test_sub_ok(self, args, expected_result): 42 | res = sub(*args) 43 | assert res == expected_result 44 | --------------------------------------------------------------------------------