Thalassa is a production-style batch data engineering project for Greek maritime traffic analytics.
It uses Bruin to orchestrate a fully scheduled pipeline that ingests public sailing traffic data from the
data.gov.gr sailing_traffic API, lands raw records in BigQuery, transforms them into curated analytics tables, and serves the results through a Streamlit dashboard covering operational KPIs, route patterns, and port analysis.
This repository is written to satisfy the spirit of the DE Zoomcamp course project: pick a real dataset, build an end-to-end pipeline, transform the data in a cloud warehouse, and expose it through a dashboard that is easy for reviewers to reproduce.
- Repository layout
- Quick start
- Problem statement and Dataset
- Course project requirements mapping
- Architecture
- Step-by-step reproduction
- Dashboard
- Using AI on Bruin Cloud UI
- Future improvements
- Contributing
.bruin.yml.example: Bruin connection config template.env.example: local environment template.streamlit/: copysecrets.toml.exampletosecrets.tomlfor service account authdashboard/: Streamlit dashboardinfra/: Terraform for GCP foundationnotebooks/: exploration notebookspipeline/: Bruin pipelinepyproject.toml: Python project and dependency definitionsruntime_config.py: shared runtime configuration helpersscripts/: operational helpers — dataset sync, snapshot generation, parity checksuv.lock: locked dependency versions
- Clone the repository and run
uv sync(oruv sync --extra localfor dashboard + notebooks). - Complete Authenticate to Google Cloud.
- Run the setup flow in Choose a setup path. If the dataset and infrastructure already exist, use the local-only switch in Change the dataset later instead.
- Run the initial backfill.
- Launch the dashboard.
Greek coastal traffic data is publicly available, but it is not immediately usable for analytics. Raw API responses are noisy, schema-light, and difficult to compare across time, ports, and routes.
This project answers questions such as:
- Which corridors carry the highest passenger volume?
- Which ports are the busiest over time?
- How do passenger and vehicle volumes change daily, weekly, and monthly?
- Is traffic concentrating in a few corridors?
- Which ports are arrival-heavy or departure-heavy in a given window?
To answer those questions reliably, the project builds a repeatable batch pipeline with explicit quality checks, curated warehouse models, and a dashboard backed by stable fact tables instead of ad hoc raw queries.
- Source:
data.gov.grsailing_trafficAPI - Domain: Greek maritime passenger and vehicle traffic
- Pipeline interpretation: the source is modeled as reported traffic observations by service date, route code, departure port, and arrival port
- Analytics outcome: curated route- and port-level warehouse tables for dashboarding
See docs/course_requirements.md for the full mapping of DE Zoomcamp evaluation criteria to this project.
See docs/architecture.md for diagrams, pipeline details, tech stack, warehouse layers, partitioning, and data quality checks.
Tip
Commands shown use Bash/Linux syntax. PowerShell alternatives are available in collapsibles where the commands differ.
Important
Required tools:
You need these tools available in your shell:
- Python 3.11+
uvbruinterraformgcloud
git clone https://github.com/dimzachar/thalassa-analytics.git
cd thalassa-analyticsInstall core dependencies only:
uv syncInstall with the Streamlit dashboard and DuckDB local mirror:
uv sync --extra dashboardInstall with Jupyter notebooks support:
uv sync --extra notebooksInstall everything (dashboard + notebooks + DuckDB):
uv sync --extra localYou need a GCP project with billing enabled.
At minimum, the project must support:
- BigQuery
- IAM
- Secret Manager
- Service Usage
- optional Artifact Registry
The simplest local path is Application Default Credentials.
gcloud auth application-default login
gcloud auth application-default set-quota-project YOUR_GCP_PROJECTReplace YOUR_GCP_PROJECT with your real GCP project ID.
Alternative: use a service account JSON file and export one of these variables:
Linux/macOS:
export GOOGLE_APPLICATION_CREDENTIALS=/absolute/path/to/key.json
# or
export THALASSA_GCP_SERVICE_ACCOUNT_FILE=/absolute/path/to/key.jsonWindows (PowerShell):
$env:GOOGLE_APPLICATION_CREDENTIALS = "C:\absolute\path\to\key.json"
# or
$env:THALASSA_GCP_SERVICE_ACCOUNT_FILE = "C:\absolute\path\to\key.json"For Streamlit specifically, you can also copy .streamlit/secrets.toml.example to .streamlit/secrets.toml and fill in the service account values.
If you do not already have a local .bruin.yml, create it from the example first.
Linux/macOS:
cp .bruin.yml.example .bruin.ymlWindows (PowerShell):
Copy-Item .bruin.yml.example .bruin.ymlThen update .bruin.yml so the gcp-default connection points to your project.
Minimal example:
default_environment: default
environments:
default:
connections:
google_cloud_platform:
- name: gcp-default
project_id: YOUR_GCP_PROJECT
location: EU
use_application_default_credentials: trueReplace YOUR_GCP_PROJECT with your real GCP project ID.
Pick one of these and follow only that one.
Use this when you want one command to set the dataset, sync Bruin, validate the pipeline, and apply Terraform.
Run the command from the repo root after Authenticate to Google Cloud is done.
If .bruin.yml does not exist yet, the helper creates it from .bruin.yml.example automatically.
Linux/macOS:
./scripts/set_dataset.sh my_dataset --project-id YOUR_GCP_PROJECT --region europe-west1 --bq-location EU --environment prodWindows (PowerShell):
.\scripts\set_dataset.ps1 my_dataset -ProjectId YOUR_GCP_PROJECT -Region europe-west1 -BqLocation EU -Environment prodNote
What this does:
- creates
.envfrom.env.exampleif needed - sets
THALASSA_BQ_DATASET - sets
THALASSA_BQ_PROJECTandTHALASSA_BQ_LOCATIONwhen you pass them - creates
.bruin.ymlfrom the example if needed - updates the
gcp-defaultBruin connection project and location - creates
infra/terraform.tfvarsfrom the example if needed - writes
project_id,region,bq_location, andenvironment - syncs Bruin asset dataset prefixes
- runs
bruin validate ./pipeline --fast - runs
terraform init - selects or creates a dataset-specific Terraform workspace
- runs
terraform planandapply
Replace:
my_datasetwith the dataset name you wantYOUR_GCP_PROJECTwith your real GCP project ID- Change
europe-west1,EU, andprodtoo if your region, BigQuery location, or environment are different.
The dataset name lives in .env. Terraform reads it from THALASSA_BQ_DATASET, so you do not need to repeat it in infra/terraform.tfvars.
Project-wide Terraform resource names are derived from that dataset too. For example, my_dataset becomes a service account like my-dataset-pipeline and an Artifact Registry repo like my-dataset.
Add -AutoApprove in PowerShell or --auto-approve in Bash if you want a non-interactive Terraform apply.
If you want to control .env and Terraform inputs yourself, run these commands from the repo root.
Linux/macOS:
cp .env.example .env
cp infra/terraform.tfvars.example infra/terraform.tfvarsWindows (PowerShell):
Copy-Item .env.example .env
Copy-Item infra/terraform.tfvars.example infra/terraform.tfvarsEdit .env and set at least:
THALASSA_BQ_PROJECTTHALASSA_BQ_DATASETTHALASSA_BQ_LOCATION
Then edit infra/terraform.tfvars and set at least:
project_idregionbq_locationenvironment
Then sync Bruin and apply the infrastructure:
uv run --no-project python ./scripts/sync_bruin_dataset.py
bruin validate ./pipeline --fast
terraform -chdir=infra init
terraform -chdir=infra workspace select dataset-my_dataset || terraform -chdir=infra workspace new dataset-my_dataset
terraform -chdir=infra plan
terraform -chdir=infra applySee more about what Terraform creates.
Those AI secret slots are empty placeholders. You do not need to add OpenRouter, Anthropic, or Gemini keys unless you want LLM-generated intelligence text. Without them, the project still runs and falls back to deterministic reporting.
- The
thalassa.auto_panel_snapshot_writerasset expects Bruin secret connection names to exist. - For local runs without an LLM key, add placeholder
genericconnections once:
bruin connections add --environment default --name openrouter_api_key --type generic --credentials '{}'
bruin connections add --environment default --name openrouter_model --type generic --credentials '{"value":"google/gemini-2.0-flash-exp:free"}'
bruin connections add --environment default --name openrouter_base_url --type generic --credentials '{"value":"https://openrouter.ai/api/v1"}'- If your
.bruin.ymlwas created from.bruin.yml.example, these placeholders already exist and you can skip these commands. - These can be empty; with no API key, snapshot generation falls back to deterministic mode.
For a first run, use --full-refresh. This is especially important if tables already exist and you want BigQuery partitioning and clustering to match the checked-in model definitions.
uv run --no-project python ./scripts/sync_bruin_dataset.py
bruin run --full-refresh ./pipeline/assets/ingestion/raw_sailing_traffic.py --downstream --start-date 2025-01-01 --end-date 2025-01-31 --var 'source_data_lag_days=0'The pipeline default is source_data_lag_days=1 for scheduled runs.
For manual backfills, pass --var 'source_data_lag_days=0' so the requested --start-date and --end-date are used as-is.
To backfill a larger date range, use --var 'request_window_unit="month"' to batch requests by month instead of day. The API data starts from 2017-01-03.
bruin run ./pipeline/assets/ingestion/raw_sailing_traffic.py --downstream --start-date 2017-01-01 --end-date 2023-12-31 --var 'request_window_unit="month"' --var 'source_data_lag_days=0'Note
The API enforces a 249-day maximum per request window. month (max 31 days) is safe. year will always fail with a 400.
What this does:
- fetches API data for the selected window
- lands raw rows in
<THALASSA_BQ_DATASET>.raw_sailing_traffic - builds staging, intermediate, marts, and report tables
- refreshes
<THALASSA_BQ_DATASET>.intelligence_snapshots - triggers the snapshot writer downstream
uv run streamlit run ./dashboard/app.pyOpen the local Streamlit URL shown in the terminal.
If BigQuery returns USER_PROJECT_DENIED / serviceusage.services.use:
- Local Streamlit auth uses your local identity by default (ADC user), unless you explicitly set
THALASSA_GCP_SERVICE_ACCOUNT_FILE,GOOGLE_APPLICATION_CREDENTIALS, or.streamlit/secrets.toml. - Terraform grants
roles/serviceusage.serviceUsageConsumerandroles/bigquery.jobUserto the pipeline service account (<dataset>-pipeline), not automatically to every human user in the project. - The identity used by Streamlit must have permission to use the project for quota/billing (
serviceusage.services.use) and BigQuery read access to the configured dataset.
Install Bruin Visualizer (source) for an interactive DAG view with impact analysis and run history:
uv tool install bruin-visualizer
# Parse the pipeline (generates pipeline_graph.json)
bruin-viz parse ./pipeline
# Open the visualizer at http://localhost:8001
bruin-viz serveTo also record run history before serving:
bruin-viz run ./pipeline --start-date 2025-01-01 --end-date 2025-01-31 --workers 1
bruin-viz parse ./pipeline
bruin-viz serveOnce the project is already set up, use the same script to switch datasets.
For a local-only switch without Terraform:
Linux/macOS:
./scripts/set_dataset.sh my_dataset --skip-terraformWindows (PowerShell):
.\scripts\set_dataset.ps1 my_dataset -SkipTerraformReplace my_dataset with the dataset name you want.
If the new dataset also needs Terraform changes applied, rerun the command from One-click setup (recommended) without the skip flag.
uv run --no-project python ./scripts/sync_bruin_dataset.py
bruin validate ./pipeline --fast
bruin validate ./pipelineThese commands are useful when you want to refresh only the narrative layer.
bruin run ./pipeline/assets/reports/intelligence_snapshots.sql --downstreamor
uv run python ./scripts/generate_auto_panel_snapshot.pyAt this point you should have:
- BigQuery tables under your configured
THALASSA_BQ_DATASET - a dashboard that loads KPI cards and charts successfully
- visible categorical and temporal tiles for course review
A quick warehouse sanity query is:
SELECT
MAX(service_date) AS latest_service_date,
SUM(total_passengers) AS passengers,
SUM(total_vehicles) AS vehicles
FROM `YOUR_GCP_PROJECT.YOUR_THALASSA_BQ_DATASET.row_counts_daily`;Replace YOUR_GCP_PROJECT and YOUR_THALASSA_BQ_DATASET with your real values.
For a full set of ready-to-run inspection queries (row counts, date ranges, top routes, top ports, and more), see docs/thalassa_queries.sql.
See more about how cleanup works.
terraform -chdir=infra workspace select dataset-YOUR_THALASSA_BQ_DATASET
terraform -chdir=infra destroyIf you want Terraform to delete the dataset contents too:
- set
dataset_delete_contents_on_destroy = trueininfra/terraform.tfvars - run
terraform -chdir=infra workspace select dataset-YOUR_THALASSA_BQ_DATASET - run
terraform -chdir=infra destroy
The dashboard reads curated BigQuery tables, not raw API payloads. The default view window is the last 90 days, giving an at-a-glance picture of recent traffic.
Dashboard query caching is data-aware: cached reads are automatically invalidated when key warehouse tables (row_counts_daily, fct_route_traffic_daily, fct_port_activity_daily, intelligence_snapshots) are updated.
The main page shows:
- KPI cards for total passengers, total vehicles, active corridors, traffic concentration, and the leading port
Traffic Pulse: a daily trend chart for passengers, vehicles, and sailings over the view windowTop Corridors: a ranked view of the busiest departure-arrival pairs by passenger volumePort Balance: a ranked view of the busiest ports by total passenger flow- (Optional) A cached intelligence panel backed by
<THALASSA_BQ_DATASET>.intelligence_snapshots— see docs/agent_panel.md for how the snapshot pipeline, LLM gating, and fallback logic work
The Analytics page lets users filter by date range, port, and corridor to explore:
- weekday traffic profile
- corridor concentration over time
- corridor efficiency metrics
- port net flow (arrivals vs departures)
This means the course dashboard requirement is covered by at least:
- one temporal tile:
Traffic Pulse - one categorical tile:
Top CorridorsorPort Balance
If you're already running this pipeline, you can connect it to Bruin Cloud and query your data conversationally through a built-in AI agent.
Connect the project:
Go to Team Settings → Projects and add this repo. Once linked, enable the pipeline to run and backfill from the Cloud UI.
Create an AI agent:
Go to Agents → Create New Agent, select the repo and configure any credentials you need (e.g. Slack). You will also need to configure connections and then start using it.
What you can do:
- Chat with the agent inside the UI to ask questions about the data
- Review the SQL it generates before it runs
- Connect to Slack and query from there too
- Ask the agent to generate a PDF report directly in chat
Pipeline monitoring:
The Cloud UI also doubles as a pipeline dashboard — you can track runs, failures, and lineage without leaving the browser.
Scheduled lag setting on Bruin Cloud:
- The pipeline default is
source_data_lag_days=1because the source publishes data with a one-day lag, so scheduled runs target the latest available complete date. - For manual backfills where you want literal dates, pass
--var 'source_data_lag_days=0'. - Re-running the same interval with
strategy: appendcan append duplicate rows inraw_sailing_traffic(downstream models deduplicate byrecord_hash).
Enable LLM overlay in scheduled pipeline snapshots:
- Create three
Generic Secretconnections in Bruin Cloud: openrouter_api_key→ your OpenRouter API keyopenrouter_model→ your preferred OpenRouter model id (optional)openrouter_base_url→https://openrouter.ai/api/v1- The
thalassa.auto_panel_snapshot_writerasset injects these asOPENROUTER_API_KEY,OPENROUTER_MODEL, andOPENROUTER_BASE_URL. - Verify from run logs:
mode=llm_overlayindicates the LLM path was used.
- Add CI checks for
bruin validate, dependency sync, and Terraform planning - Add automated tests
- Add monitoring
Contributions are welcome, especially around data quality, dashboard UX, testing, and deployment hardening.
Before opening a PR:
- Keep the change focused on one concern
- Run
uv sync(oruv sync --extra local) if dependencies changed - Run
uv run --no-project python ./scripts/sync_bruin_dataset.py - Run
bruin validate ./pipeline - If you changed SQL models or ingestion logic, run the relevant
bruin runcommand for a representative date window - If you changed the dashboard, include a screenshot or short note describing the UI impact
- In the PR description, include the purpose of the change, touched paths, and validation commands you ran
For larger changes, opening an issue first is the best way to align on scope before implementation.



