- Refactored init & features

- Extended documentation
This commit is contained in:
2024-07-14 22:27:32 +02:00
parent 2d4bdbceeb
commit 02a82589ae
50 changed files with 16416 additions and 224 deletions

View File

@@ -0,0 +1 @@
# Commands

View File

@@ -0,0 +1,19 @@
---
sidebar_position: 1
---
# Init
```
Description:
Initialize a new project
Usage:
m4g init <name> [options]
Arguments:
<name> The name of your project
Options:
--without <api|db|git> Features to exclude
-?, -h, --help Show help and usage information

View File

@@ -0,0 +1,19 @@
---
sidebar_position: 1
---
# Install
```
Description:
Install packages and update the requirements.txt
Usage:
m4g install [<packages>...] [options]
Arguments:
<packages> The names of the packages to install
Options:
-?, -h, --help Show help and usage information
```

20
docs/docs/install.md Normal file
View File

@@ -0,0 +1,20 @@
---
sidebar_position: 2
---
# Install
## Requirements
MycroForge has the following dependencies.
- bash
- git
- Python3 (3.10)
- Docker
### Windows
To simplify the implementation of this tool, it assumes that it's running in a POSIX compliant environment.
MycroForge has been developed and tested on Windows in WSL2 Ubuntu 22.04.03.
So when running on Windows, it's recommended to run MycroForge in the same environment or atleast in a similar WSL2 distro.

145
docs/docs/intro.md Normal file
View File

@@ -0,0 +1,145 @@
---
sidebar_position: 1
---
# Intro
## 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 <name>`
```
Description:
Initialize a new project
Usage:
m4g init <name> [options]
Arguments:
<name> The name of your project
Options:
--without <api|db|git> Features to exclude
-?, -h, --help Show help and usage information
```
Running this command will generate the following project structure.
```
📦<project_name>
┣ 📂.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.
## Plugin system
TODO: Dedicate a section to the Plugin system

283
docs/docs/tutorial.md Normal file
View File

@@ -0,0 +1,283 @@
---
sidebar_position: 3
---
# 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!