From 6301bd438a1998ca058fe71dab75304ed9817be4 Mon Sep 17 00:00:00 2001 From: mdnapo Date: Thu, 25 Jul 2024 07:35:06 +0200 Subject: [PATCH] Additional clean up --- readme/README.md | 141 ----------------- readme/todo-example/README.md | 279 ---------------------------------- 2 files changed, 420 deletions(-) delete mode 100644 readme/README.md delete mode 100644 readme/todo-example/README.md diff --git a/readme/README.md b/readme/README.md deleted file mode 100644 index fffe18d..0000000 --- a/readme/README.md +++ /dev/null @@ -1,141 +0,0 @@ -# General introduction - -## What is MycroForge? - -MycroForge is an opinionated CLI tool that is meant to facilitate the development of FastAPI & SQLAlchemy based backends. -It provides a command line interface that allows users to generate a skeleton project and other common items like -database entities, migrations, routers and even basic CRUD functionality. The main purpose of this tool is to generate -boilerplate code and to provide a unified interface for performing recurrent development activities. - - -## Generating a project - -To generate a project you can run the following command. - -`m4g init ` - -``` -Description: - Initialize a new project - -Usage: - m4g init [options] - -Arguments: - The name of your project - -Options: - --without Features to exclude - -?, -h, --help Show help and usage information - -``` - -Running this command will generate the following project structure. - -``` -📦 - ┣ 📂.git - ┣ 📂.venv - ┣ 📂api - ┃ ┗ 📂routers - ┃ ┃ ┗ 📜hello.py - ┣ 📂db - ┃ ┣ 📂engine - ┃ ┃ ┗ 📜async_session.py - ┃ ┣ 📂entities - ┃ ┃ ┗ 📜entity_base.py - ┃ ┣ 📂versions - ┃ ┣ 📜README - ┃ ┣ 📜env.py - ┃ ┣ 📜script.py.mako - ┃ ┗ 📜settings.py - ┣ 📜.gitignore - ┣ 📜alembic.ini - ┣ 📜db.docker-compose.yml - ┣ 📜m4g.json - ┣ 📜main.py - ┗ 📜requirements.txt -``` - -Let's go through these one by one. - -### .git - -The `m4g init` command will initialize new projects with git by default. -If you don't want to use git you can pass the option `--without git` to `m4g init`. - -### .venv - -To promote isolation of Python dependencies, new projects are initialized with a virtual environment by default. -TODO: This is a good section to introduce the `m4g hydrate` command. - -### api/routers/hello.py - -This file defines a basic example router, which is imported and mapped in `main.py`. This router is just an example and -can be removed or modified at you discretion. - -### db/engine/async_session.py - -This file defines the `async_session` function, which can be used to open an asynchronous session to a database. - -### db/entities/entity_base.py - -This file contains an automatically generated entity base class that derives from the DeclarativeBase. -All entities must inherit from this class, so that SQLAlchemy & alembic can track them. The entities directory is also -where all newly generated entities will be stored. - -### db/versions - -This is where the generated database migrations will be stored. - -### db/README - -This README file is automatically generated by the alembic init command. - -### db/env.py - -This is the database environment file that is used by alembic to interact with the database. -If you take a closer look at the imports, you'll see that the file has been modified to assign `EntityBase.metadata` to -a variable called `target_metadata`, this will allow alembic to track changes in your entities. You'll also find that -the `DbSettings` class is used to get the connectionstring. Any time you generate a new database entity, or create a -many-to-many relation between two entities, this file will also be modified to include the generated classes. - -### db/script.py.mako - -This file is automatically generated by the alembic init command. - -### db/settings.py - -This file defines the `DbSettings` class, that is responsible for retrieving the database connectionstring. -You will probably want to modify this class to retrieve the connectionstring from a secret manager at some point. - -### .gitignore - -The default .gitignore file that is generated by the `m4g init` command. Modify this file at your discretion. - -### alembic.ini - -This file is automatically generated by the alembic init command. - -### db.docker-compose.yml - -A docker compose file for running a database locally. - -### m4g.json - -This file contains some configs that are used by the CLI, for example the ports to map to the API and database. - -### main.py - -The entrypoint for the application. When generating entities, many-to-many relations or routers, this file will be -modified to include the generated files. - -### requirements.txt - -The requirements file containing the Python dependencies. -TODO: introduce the `m4g install` & `m4g uninstall` commands. - - -## Scripting - -TODO: Dedicate a section to scripting \ No newline at end of file diff --git a/readme/todo-example/README.md b/readme/todo-example/README.md deleted file mode 100644 index f94b80b..0000000 --- a/readme/todo-example/README.md +++ /dev/null @@ -1,279 +0,0 @@ -# Quick tutorial - -We're going to build a simple todo app, to demonstrate the capabilities of the MycroForge CLI. -After this tutorial, you should have a solid foundation to start exploring and using MycroForge to develop your -projects. - -## General notes - -The commands in this tutorial assume that you're running them from a MycroForge root directory. - -## Initialize the project - -Open a terminal and `cd` into the directory where your project should be created. -Run `m4g init todo-app` to initialize a new project and open the newly created project by running `code todo-app`. -Make sure you have `vscode` on you machine before running this command. If you prefer using another editor, then -manually open the generated `todo-app` folder. - -## Setup the database - -Our todo app needs to keep track of todos, so it needs a storage mechanism of some sorts. A database should be one -of the first things, if not THE first thing, that comes to mind. Luckily, MycroForge provides you with a locally hosted -database for you project out of the box. This setup, is powered by docker compose and can be examined by opening the -`db.docker-compose.yml` file in the project. Follow along to learn how to use the docker based database when developing -locally. - -### Run the database - -The first step is to start the database, you can do this by running the following command in a terminal. - -`m4g db run` - -This command starts the services defined in the `db.docker-compose.yml` file. -You can verify that the services are up by running `docker container ls`. If everything went well, then the previous -command should output the service names defined in `db.docker-compose.yml`. - -If you go to [PhpMyAdmin (i.e. http://localhost:5051)](http://localhost:5051), you should now be able to login with the -following credentials. -- user: root -- pass: password - -When you're done developing, you can shut down the local database by running `m4g db stop` - - -### Create the entities - -Now that the database is running, we can start to create our entities. Run the commands below to create the `Todo` & -`Tag` entities. - -`m4g db generate entity Tag --column "description:str:String(255)"` - -`m4g db generate entity Todo --column "description:str:String(255)" -c "is_done:bool:Boolean()"` - -After running these commands, you should find the generated entities in the `db/entities` folder of your project. -You should also see that the `main.py` & `db/env.py` files have been modified to include the newly generated entity. - -For more information about the `m4g db generate entity` command, you can run `m4g db generate entity -?`. - -### Define a many-to-many relation between Todo & Tag - -To allow for relations between `Todo` & `Tag`, we'll define a many-to-many relation between the two entities. -This relation makes sense, because a `Todo` can have many `Tags` and a `Tag` could belong to many `Todos`. -You can generate this relation by running the following command from you terminal. -Creating a one-to-many relation would also make sense, but for the purpose of demonstration we're going to demonstrate -the many-to-many relation, because this one is the most complex, since it requires an additional mapping to be included -in the database schema. - -`m4g db link many Todo --to-many Tag` - -After running this command you should see that both the `Todo` and `Tag` entities now have a new field referencing the -a `List` containing instances of the other entity. - -For more information about the `m4g db link` command try running `m4g db link -?`. Note that you can do the same thing -for all sub commands, so if you want to know more about `m4g db link many` you can simply run `m4g db link many -?` to -examine the command. The same is true for all the other commands as well. - -### Generate the migration - -Now that we've generated our entities, it's time to generate a migration that will apply these changes in the database. -Generate the initial migration by running the following command. - -`m4g db generate migration initial_migration` - -After running this command, you should see the new migration in the `db/version` directory. - -### Apply the migration - -The last step for the database setup is to actually apply the new migration to the database. This can be done by running -the following command. - -`m4g db migrate` - -After running this command, you should now see a populated schema when visiting [PhpMyAdmin](http://localhost:5051). -If for whatever reason you want to undo the last migration, you can simply run `m4g db rollback`. - -## Setup the API - -### Generate CRUD for Tag & Todo - -Our API should provide use with basic endpoint to manage the `Todo` & `Tag` entities, i.e. CRUD functionality. -Writing this code can be boring, since it's pretty much boilerplate with some custom additions sprinkled here and there. -Fortunately, MycroForge can generate a good chunk of this boring code on your behalf. Run the following commands to -generate CRUD functionality for the `Todo` & `Tag` classes. - -`m4g api generate crud Tag` - -`m4g api generate crud Todo` - -After running this command you should see that the `api/requests`,`api/routers` & `api/services` now contain the -relevant classes need to support the generated CRUD functionality. This could should be relatively straightforward, so -we won't dive into it, but feel free to take a break and explore what the generated code actually does. Another thing to -note, is that the generated routers are also automatically included in `main.py`. - -### Modify the generated Todo request classes - -Since we have a many-to-many relationship between `Todo` & `Tag`, the generated CRUD functionality isn't quite ready -yet. We need to be able to specify which `Tags` to add to a `Todo` when creating or updating it. -To do this, we will allow for a `tag_ids` field in both the `CreateTodoRequest` & the `UpdateTodoRequest`. -This field will contain the ids of the `Tags` that are associated with a `Todo`. - -Modify `CreateTodoRequest` in `api/requests/create_todo_request.py`, you might need to import `List` from `typing`. - -```python -# Before -class CreateTodoRequest(BaseModel): - description: str = None - is_done: bool = None - -# After -class CreateTodoRequest(BaseModel): - description: str = None - is_done: bool = None - tag_ids: Optional[List[int]] = [] -``` - -Modify `UpdateTodoRequest` in `api/requests/update_todo_request.py`, you might need to import `List` from `typing`. - -```python -# Before -class UpdateTodoRequest(BaseModel): - description: Optional[str] = None - is_done: Optional[bool] = None - tag_ids: Optional[List[int]] = [] - -# After -class UpdateTodoRequest(BaseModel): - description: Optional[str] = None - is_done: Optional[bool] = None - tag_ids: Optional[List[int]] = [] -``` - -### Modify generated TodoService - -The `TodoService` will also need to be updated to accomodate the management of `tag_ids`. - -Add the following imports in `api/services/todo_service.py`. - -```python -from sqlalchemy.orm import selectinload -from db.entities.tag import Tag -``` - -Modify `TodoService.list` - -```python -# Before - async with async_session() as session: - stmt = select(Todo) - results = (await session.scalars(stmt)).all() - return results - -# After - async with async_session() as session: - stmt = select(Todo).options(selectinload(Todo.tags)) - results = (await session.scalars(stmt)).all() - return results -``` - -Modify `TodoService.get_by_id` - -```python -# Before - async def get_by_id(self, id: int) -> Optional[Todo]: - async with async_session() as session: - stmt = select(Todo).where(Todo.id == id) - result = (await session.scalars(stmt)).first() - return result - -# After - async def get_by_id(self, id: int) -> Optional[Todo]: - async with async_session() as session: - stmt = select(Todo).where(Todo.id == id).options(selectinload(Todo.tags)) - result = (await session.scalars(stmt)).first() - return result -``` - -Modify `TodoService.create` - -```python -# Before - async def create(self, data: Dict[str, Any]) -> None: - async with async_session() as session: - entity = Todo(**data) - session.add(entity) - await session.commit() - -# After - async def create(self, data: Dict[str, Any]) -> None: - tag_ids = [] - - if "tag_ids" in data.keys(): - tag_ids = data["tag_ids"] - del data["tag_ids"] - - async with async_session() as session: - entity = Todo(**data) - - if len(tag_ids) > 0: - stmt = select(Tag).where(Tag.id.in_(tag_ids)) - result = (await session.scalars(stmt)).all() - entity.tags = list(result) - - session.add(entity) - await session.commit() -``` - -Modify `TodoService.update` - -```python -# Before - async def update(self, id: int, data: Dict[str, Any]) -> bool: - tag_ids = [] - - if "tag_ids" in data.keys(): - tag_ids = data["tag_ids"] - del data["tag_ids"] - - async with async_session() as session: - stmt = select(Todo).where(Todo.id == id) - entity = (await session.scalars(stmt)).first() - - if entity is None: - return False - else: - for key, value in data.items(): - setattr(entity, key, value) - await session.commit() - return True - -# After - async def update(self, id: int, data: Dict[str, Any]) -> bool: - tag_ids = [] - - if "tag_ids" in data.keys(): - tag_ids = data["tag_ids"] - del data["tag_ids"] - - async with async_session() as session: - stmt = select(Todo).where(Todo.id == id).options(selectinload(Todo.tags)) - entity = (await session.scalars(stmt)).first() - - if entity is None: - return False - else: - for key, value in data.items(): - setattr(entity, key, value) - - if len(tag_ids) > 0: - stmt = select(Tag).where(Tag.id.in_(tag_ids)) - result = (await session.scalars(stmt)).all() - entity.tags = list(result) - else: - entity.tags = [] - - await session.commit() - return True -``` - -At this point, the app should be ready to test. -TODO: Elaborate! \ No newline at end of file