Polyglot Programming

Learn C#, Python, and Go through practical examples

Init command

Implement the init command

go python

Exercise

We will implement the functionality of the init command. The init is responsible of creating the SQLite database if it does not exists and to create the tables in the database. If a database is already exists, the init command should not do anything. The database should be created in a hidden folder called .envtamer in the users’s home directory. The database should be calles envtamer.db.

SQL

CREATE TABLE IF NOT EXISTS "EnvVariables" (
  "Directory" TEXT NOT NULL,
  "Key" TEXT NOT NULL,
  "Value" TEXT NOT NULL,
  CONSTRAINT "PK_EnvVariables" PRIMARY KEY ("Directory", "Key")
);

Messages

πŸ›‘ Database file already exists. Initialization skipped
πŸ—„οΈ Empty database file created successfully
⏳ Running migrations...
βœ… Migrations applied successfully
πŸš€ Ready to push and pull env files
πŸ›‘ Error creating database:

Go

For the interaction with the SQLite database we will need a driver. Add it to the project as follows. The command should be executed in the root of the Go project, where the go.mod file is.

go get -u modernc.org/sqlite

The code for interacting with the database should be added to the sqlite.go file in the storage folder

β”œβ”€β”€ LICENSE
β”œβ”€β”€ README.md
β”œβ”€β”€ cmd
β”‚Β Β  └── envtamer-go
β”‚Β Β      └── main.go
β”œβ”€β”€ go.mod
β”œβ”€β”€ go.sum
└── internal
    β”œβ”€β”€ command
    β”‚Β Β  └── ...
    β”œβ”€β”€ storage
    β”‚Β Β  └── sqlite.go
    └── util

To implement the init command, we need a constructor in the sqlite.go and an Init function. That function should be implemented as a method receiver on the Storage struct.

type Storage struct {
	db *sql.DB
}

The type sql.DB comes from the standard library and is a struct you can use to interact with the database.

Python

To implement the init command we will need to create a SQLite database and a table in it. The database should be created in a hidden folder called .envtamer in the users’s home directory. The commands of the cli will be implemented in the cli.py file. The cli.py file will be the entry point of the project. The cli.py file will import the init command and call it when the user runs the init command. We will us the click package to implement the cli. The click package is a package that makes it easy to create command line interfaces in Python. You can find the documentation here: Documentation. The click package is already added to the project. The code for the init command should be added to the init_command.py file in the envtamer folder. The init_command.py file will contain the code for the init command. The init command will be imported in the cli.py file and called when the user runs the init command.

I found it best to use a group command for the cli. This way we can add subcommands to the cli. The group command is a command that can have subcommands. The group command will be the entry point of the project, and provide the configuration for the options. The group command will be imported in the cli.py file and called when the user runs the init command.

For the interaction with the SQLite database we will be using an ORM. It is called SQLAlchemy and you can find the implementation details here: [Documentation] (https://www.sqlalchemy.org/). It will be added in the project in it’s own package. Add the package by creating a new folder in the root of the project and name it envtamer_db and place a init.py file in it. The init.py file is required to make the folder a package. The init.py file should be empty. The code for interacting with the database should be added to the envtamer_db folder.

add the following packages to the project.

poetry add sqlalchemy
poetry add sqlalchemy-utils

create a new file in the envtamer_db folder and name it envtamer_db.py. This file will contain the code for interacting with the database.

The envtamer_db.py file will be using the following:

create_engine(f"sqlite:///{self.db_file}")

#create DB
if not database_exists(engine.url):
    create_database(engine.url)
    Base.metadata.create_all(engine)

with Session(self.engine) as env_session:
    #do stuff

    #commit the changes
    env_session.commit()

The table will be created the first tima you save data to the database.

create a new file in the envtamer_db folder and name it env_variable.py. This file will contain the code for the EnvVariable model.

the env_variable.py file should contain the code for the EnvVariable model. The EnvVariable model will be used to interact with the database. The EnvVariable model will be used to create the table in the database. it will look something like this:

from sqlalchemy import UniqueConstraint
from sqlalchemy import String
from sqlalchemy.orm import DeclarativeBase
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column

class Base(DeclarativeBase):
    pass

class EnvVariable(Base):
    #your implementation here

your project structure should look like this:

β”œβ”€β”€ envtamer
β”‚Β Β  └── __init__.py
β”‚Β Β  β”œβ”€β”€ cli.py
β”‚Β Β  └── init_command.py
β”œβ”€β”€ envtamer_db
β”‚Β Β  └── __init__.py
β”‚Β Β  β”œβ”€β”€ env_variable.py
β”‚Β Β  └── envtamer_db.py
β”œβ”€β”€ tests
β”‚Β Β  └── __init__.py
β”œβ”€β”€ poetry.lock
β”œβ”€β”€ pyproject.toml
└── README.md

To use the cli we must add the following to the pyproject.toml file:

packages = [
    { include = "*.py" },
    { include = "envtamer" },
    { include = "envtamer/*.py" },
    { include = "envtamer_db" },
    { include = "envtamer_db/*.py" },
]

[tool.poetry.scripts]
envtamer = "envtamer.cli:cli"

the packages section is used to include the packages in the project. The scripts section is used to add the cli command to the project. The cli command will be used to run the cli.

To help you along, this is a snippet of my cli.py file:


@click.group()
@click.option('--directory', '-d', help='Name of the directory to push. Defaults to current working directory if not specified.')
@click.option('--file_name', '-f', default='.env', help='file name of the env file. Defaults to \'.env\' in the specified or current directory.')
@click.pass_context
def cli(ctx, directory, file_name):
    # ensure that ctx.obj exists and is a dict (in case `cli()` is called
    # by means other than the `if` block below)
    ctx.ensure_object(dict)
    ctx.obj['DIRECTORY'] = directory
    ctx.obj['FILE_NAME'] = file_name

@cli.command('init')
def do_init():
  pass

if __name__ == '__main__':
    cli(obj={})

The @click.pass_context is important, it’s used to make the context available to the command. The context is used to pass the options to the command. The context is passed to the command as an argument. The context is a dictionary that contains the options and arguments passed to the command. The context is created by the click package and passed to the command.

Solution

Solution
git clone --branch 04-init-command --single-branch https://github.com/XPRTZ/envtamer.git