Skip to content

Directory Structure

Previously, in the "Getting Started" section, we showed than a valid DBgen pipeline can be written in 9 lines of code; however, in most real cases, a significantly more complex data pipeline is required.

Let's start by walking through the directory structure of a typical DBgen project.

You can follow along with this tutorial by copying some boiler plate code by running the command:

$ dbgen new --template alice-bob-lab
<span style='color: green;'>Downloading template from https://github.com/modelyst/dbgen-model-template....</span>

This will prompt you to download the relevant files to a local directory with the directory structure shown below. The last prompt asks whether you want to download the finished tutorial or only the skeleton of it so that you can complete it yourself. You can find all the model templates at the DBgen Repo.

├── data
├── alice_bob_model
    ├── extracts
    │   ├── __init__.py
    │   ├── extract_1.py
    ├── etl_steps
    │   ├── __init__.py
    │   ├── etl_step_1.py
    │   ├── etl_step_2.py
    ├── transforms
    │   ├── __init__.py
    │   ├── transform_1.py
    ├── __init__.py
    ├── constants.py
    ├── main.py
    ├── schema.py

main.py

The overall goal of this entire repository is to create a DBgen model, which specifies the procedure to build the empty database and also to populate it with data. In main.py, there is a function that returns this DBgen model that the rest of the repository defines. This is a boiler-plate function, and a user does not need to ever change anything in this file.

The contents of main.py are shown below.

from alice_bob_model.etl_steps import add_etl_steps

from dbgen import Model


def make_model():
    model = Model(name="alice_bob_lab")
    add_etl_steps(model)
    return model

constants.py

This is a place to store any constants that are specific to the DBgen model defined in this repository. Common constants are:

  • The path to a data source (if the data is stored locally)
  • A cloud service (e.g. AWS) profile name and cloud storage location (if the data is stored remotely)
  • The default python environment in which most of the functions will be run (more detail on defining python environments is later in this tutorial)

The contents of constants.py used in this tutorial are shown below.

from pathlib import Path

from dbgen import Environment, Import

ROOT_DIR = Path(__file__).parent.parent
DATA_DIR = ROOT_DIR / "data"

DEFAULT_ENV = Environment([Import("typing", ["List", "Tuple"])])

schema.py

This is the file that specifies the empty database schema. In other words, this is where we define the tables, columns (and their data type), and foreign keys in the database.

Part of the schema.py used in this tutorial is shown below.

from typing import Optional

from dbgen import Entity, IDType


class Person(Entity, table=True):
    __tablename__ = "person"
    __identifying__ = {"first_name", "last_name"}
    first_name: str
    last_name: str
    age: Optional[int]

extracts

Extracts are relatively small pieces of code that define how to read data from a custom data source. We write that code here, then import it when we are writing ETLSteps later.

Below, we show an example of an extract that reads a csv stored in the local file system and returns its contents one row at a time. We'll walk through this in more detail later in the tutorial.

The code below is from extracts/csv_extract.py.

import csv
from typing import List

from dbgen import Extract


class CSVExtract(Extract):
    data_dir: str
    outputs: List[str] = ["row"]

    def extract(self):
        with open(self.data_dir) as csv_file:
            reader = csv.reader(csv_file)
            yield from reader

transforms

In the transforms module, we write functions that parse or analyze incoming data. If the same function will be used multiple times, it is best to write the function in the transforms module and import it later when the ETLStep is written.

However, if the function is specific to a particular ETLStep and will not be reused, it is common to define that transform in the same file as the ETLStep (in the etl_steps module) instead of in the transforms module.

An example of a transform is shown below. The code below is an excerpt from etl_steps/read_csv.py.

@transform(outputs=["first_name", "last_name", "age"], env=DEFAULT_ENV)
def parse_names(row: List[str]) -> Tuple[str, str, int]:
    first_name = row[0]
    last_name = row[1]
    age = int(row[2])

etl_steps

The etl_steps module is where the code that populates the database is defined. Each ETLStep is one "Extract, Transform, Load" (ETL) step. The "extract" portion of the ETL step defines the source of the data. The "transform" defines which functions will be used to parse or analyze the data, and the "load" step defines where in the database the results will be stored.

The extract step may be a custom extract that we defined earlier, or it may be a query on the database.

The purpose of ETLSteps is essentially to define where data will come from, which function will analyze it, and where it will go.

Whenever we write a new ETLStep, we write a function that accepts the model as an input and adds that ETLStep to the model. Example code is shown below (an excerpt from etl_steps/read_csv.py)

def add_io_etl_step(model: Model) -> None:
    with model:
        with ETLStep(name="names"):
            row = CSVExtract(data_dir=join(DATA_DIR, "names.csv")).results()  # extract
            first_name, last_name, age = parse_names(row).results()  # transform
            Person.load(insert=True, first_name=first_name, last_name=last_name, age=age)  # load

etl_steps.__init__.py

This is where we tell main.py to add the ETLSteps to the model. You can see that in main.py, we import a function called add_etl_steps from the etl_steps.__init__.py file. Then, add_etl_steps is the only function called within make_model().

When we finish writing a new ETLStep, the last thing to is to add it to etl_steps.__init__.py so that main.py picks it up. The pattern is simple: for each ETLStep, import the function that adds that ETLStep to the model, and call that function in add_etl_steps() as shown below (from etl_steps/__init__.py).

# Add etl_steps here
from alice_bob_model.etl_steps.f_to_c import add_f_to_c
from alice_bob_model.etl_steps.parse_measurements import add_temperature_etl_step
from alice_bob_model.etl_steps.read_csv import add_io_etl_step

from dbgen import Model


def add_etl_steps(model: Model) -> None:
    add_io_etl_step(model)
    add_temperature_etl_step(model)
    add_f_to_c(model)