11 KiB
sidebar_position |
---|
3 |
Tutorial
In this tutorial, we'll build a simple todo app to demonstrate the capabilities of the MycroForge CLI. By the end, you should have a solid foundation to start exploring and using MycroForge for your projects.
General notes
The commands in this tutorial assume that you are running them from the root directory of your MycroForge project.
Initialize the Project
Open a terminal and navigate (cd
) to the directory where your project should be created.
Run the following command to initialize a new project and open it in VSCode:
m4g init todo-app
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
.
Go to PhpMyAdmin (i.e. 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
:::info
If you're running on MacOS, Docker might complain about a platform mismatch for PhpMyAdmin.
In that case you might need to specify the platform for the PhpMyAdmin image.
You can do this by passing the --dbu-platform
flag to m4g init
.
Run m4g init -?
for all the available options.
If you've already initialized a project, you can also change the platform prefix of the PhpMyAdmin image in the db.docker-compose.yml
.
:::
Create the entities
Now that the database is running, we can start creating 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.
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
.
# Before
from pydantic import BaseModel
class CreateTodoRequest(BaseModel):
description: str = None
is_done: bool = None
# After
from typing import List, Optional
from pydantic import BaseModel
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
.
# Before
from pydantic import BaseModel
from typing import Optional
class UpdateTodoRequest(BaseModel):
description: Optional[str] = None
is_done: Optional[bool] = None
# After
from pydantic import BaseModel
from typing import List, Optional
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
.
from sqlalchemy.orm import selectinload
from db.entities.tag import Tag
Modify TodoService.list
# Before
async def list(self) -> List[Todo]:
async with async_session() as session:
stmt = select(Todo)
results = (await session.scalars(stmt)).all()
return results
# After
async def list(self) -> List[Todo]:
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
# 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
# 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
# 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
Test the API!
Run the following command.
m4g api run
Go to http://localhost:5000/docs and test your Todo API!