Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 4 additions & 11 deletions dags/repolytics_daily.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@

logger = logging.getLogger(__name__)

# Location of the dbt project inside the container
DBT_PROJECT_DIR = Path(os.environ.get("DBT_PROJECT_DIR", "/opt/airflow/dbt"))


Expand Down Expand Up @@ -65,11 +64,10 @@ def log_task_failure(context) -> None:
def repolytics_daily():
@task(multiple_outputs=False)
def ingest_github() -> dict[str, int]:
"""Run GitHub incremental ingestion into the DuckDB `raw` dataset.
"""Run GitHub incremental ingestion, returning per-table row counts.

Skipped on historical backfill runs: the dlt cursor is global (not
per-interval), so replaying old intervals for GitHub is meaningless.
Returns per-table row counts (pushed to XCom for the summary task).
Skipped on historical backfill runs: the dlt cursor is global, so replaying
old intervals is meaningless.
"""
from airflow.sdk import get_current_context

Expand All @@ -86,12 +84,7 @@ def ingest_github() -> dict[str, int]:

@task(multiple_outputs=False)
def ingest_pypi() -> dict[str, int]:
"""Run PyPI ingestion for the run's data-interval day (BigQuery).

Uses `data_interval_start`, so each run loads exactly one day and
the DAG backfills cleanly per interval. Returns per-table row counts
(pushed to XCom for the summary task).
"""
"""Run PyPI ingestion for the run's `data_interval_start` day."""
from airflow.sdk import get_current_context

from repolytics.ingestion.pipeline import run_pypi
Expand Down
12 changes: 3 additions & 9 deletions dashboard/lib/data.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,7 @@


def connect(duckdb_path: str | Path | None = None) -> duckdb.DuckDBPyConnection:
"""Open a read-only DuckDB connection to the warehouse.

Defaults to `Settings.duckdb_path`.
"""
"""Open a read-only DuckDB connection to the warehouse."""
path = Path(duckdb_path) if duckdb_path is not None else get_settings().duckdb_path
return duckdb.connect(str(path), read_only=True)

Expand All @@ -35,11 +32,8 @@ def has_marts(conn: duckdb.DuckDBPyConnection) -> bool:


def date_bounds(conn: duckdb.DuckDBPyConnection) -> tuple[date, date]:
"""Min/max event date across all activity facts, for the sidebar date filter.

Spans commits, PRs (opened), issues (opened), and releases so the slider covers
every date-filtered chart - a repo whose PRs/releases extend past its last commit
is not clipped.
"""Min/max event date across all activity facts (commits, PRs, issues, releases),
for the sidebar date filter, so no date-filtered chart is clipped.
"""
row = conn.execute(
"""
Expand Down
19 changes: 7 additions & 12 deletions dashboard/lib/health.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,12 @@
"""Composite project-health score (streamlit-free, pure DataFrame -> DataFrame).

A blend of five signals, each scored **absolutely** against a fixed reference target.
That keeps a project's score stable regardless of which other projects are selected,
and comparable across sessions and over time. The two rate signals (PR merge rate,
issue close rate) are already 0..1 and are used directly; the unbounded signals
(recent commits, stars) are mapped through a saturating log transform against a
reference, and release recency through an exponential time-decay.

Input columns (one row per repo, from `queries.health_components`):
repository_name, commits_90d, pr_merge_rate, issue_close_rate,
days_since_release (nullable -> treated as never released -> 0), stars

Output adds the five `c_*` component scores (0..1) and `health_score` (0..100).
A weighted blend of five signals, each scored **absolutely** against a fixed
reference so a project's score is stable regardless of the current selection. Rate
signals (PR merge/issue close) are used directly; unbounded signals (commits, stars)
go through a saturating log transform and release recency through exponential decay.

Input columns come from `queries.health_components`; output adds the five `c_*`
component scores (0..1) and `health_score` (0..100).
"""

import numpy as np
Expand Down
8 changes: 3 additions & 5 deletions dashboard/lib/queries.py
Original file line number Diff line number Diff line change
Expand Up @@ -368,9 +368,8 @@ def contributor_leaderboard(
def retention_cohort(conn: duckdb.DuckDBPyConnection) -> pd.DataFrame:
"""Monthly contributor retention: % of a first-active cohort active N months on.

Necessarily **repo-agnostic**: that mart's grain is contributor x month across all
tracked projects (no repository_key), so retention spans the whole tracked set and
cannot be filtered by repo. First activity across the set sets each cohort.
Repo-agnostic: the mart's grain is contributor x month with no repository_key, so
retention spans the whole tracked set and cannot be filtered by repo.
"""
sql = """
with active as (
Expand Down Expand Up @@ -404,8 +403,7 @@ def retention_cohort(conn: duckdb.DuckDBPyConnection) -> pd.DataFrame:
def active_contributors_monthly(conn: duckdb.DuckDBPyConnection) -> pd.DataFrame:
"""Monthly active contributors, split into new vs. returning.

Repo-agnostic for the same reason as `retention_cohort` (the mart has no
repository grain).
Repo-agnostic like `retention_cohort` (the mart has no repository grain).
"""
sql = """
with monthly as (
Expand Down
9 changes: 3 additions & 6 deletions dbt/macros/scd2_repository_join.sql
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
{#-
Resolve a fact repository_key against the SCD2 dim_repositories using a
half-open date range (event_date >= valid_from AND < valid_to), so each event
maps to the repository version that was current when it occurred. Centralizes
the SCD2 join contract (half-open bounds; earliest version backdated, current
version valid_to = 9999-12-31) that every fact shares. Select
`<alias>.repository_key` after using this.
Resolve a fact repository_key against SCD2 dim_repositories using a half-open date
range (event_date >= valid_from AND < valid_to), so each event maps to the
repository version current when it occurred. Select `<alias>.repository_key` after.

repo_expr - SQL expression for the repository full name (owner/name)
date_expr - SQL expression for the event date (cast to date)
Expand Down
13 changes: 5 additions & 8 deletions dbt/macros/source_or_empty.sql
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
{#-
Return `select * from <source>` when the source relation exists, otherwise an
empty result with the given columns (`select cast(null as <type>) as <col> ... where false`).
Return `select * from <source>` when the source relation exists, otherwise a typed
empty result. dlt only materializes a child table when some row populated the list,
so a list empty across every ingested row leaves the child table absent; this keeps
the build resilient to that case.

dlt only materializes a normalized child table when some row populated the
underlying list, so a list that is empty across every ingested row leaves the
child table absent. Wrapping the source in this macro keeps the build resilient
to that case.

`columns` is a mapping of column name -> SQL type, covering the columns the caller reads.
`columns` is a mapping of column name -> SQL type covering the columns the caller reads.
-#}
{% macro source_or_empty(source_name, table_name, columns) -%}
{%- set rel = source(source_name, table_name) -%}
Expand Down
7 changes: 3 additions & 4 deletions dbt/models/intermediate/int_contributor_activity.sql
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
-- Tidy contributor activity event stream at username grain: one row per discrete
-- activity event. A reusable building block for monthly aggregation (and future PR /
-- health metrics). Each PR and issue contributes up to two events (opened, and
-- merged/closed) so each lands in the month it actually happened.
-- Contributor activity event stream at username grain: one row per activity event,
-- reused for monthly aggregation. Each PR/issue contributes up to two events (opened
-- and merged/closed) so each lands in the month it happened.

with commits as (
select
Expand Down
10 changes: 5 additions & 5 deletions dbt/models/marts/_marts__models.yml
Original file line number Diff line number Diff line change
Expand Up @@ -533,17 +533,17 @@ models:
to: ref('dim_dates')
field: date_key
- name: commits
description: Commits authored that month (count, non-negative by construction).
description: Commits authored that month.
data_type: bigint
- name: prs_opened
description: Pull requests opened that month (count, non-negative by construction).
description: Pull requests opened that month.
data_type: bigint
- name: prs_merged
description: Pull requests merged that month (count, non-negative by construction).
description: Pull requests merged that month.
data_type: bigint
- name: issues_opened
description: Issues opened that month (count, non-negative by construction).
description: Issues opened that month.
data_type: bigint
- name: issues_closed
description: Issues closed that month (count, non-negative by construction).
description: Issues closed that month.
data_type: bigint
6 changes: 2 additions & 4 deletions dbt/models/marts/bridge_issue_labels.sql
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
-- Bridge table for the issue<->label many-to-many. Built from the flattened issue
-- label staging model; recomputes the issue surrogate key the same way as fct_issues
-- and resolves each label name to its label_key in dim_labels.
-- Grain: one row per (issue, label).
-- Bridge for the issue<->label many-to-many; one row per (issue, label).
-- Recomputes the issue surrogate key the same way as fct_issues.

with issue_labels as (
select * from {{ ref('stg_github__issue_labels') }}
Expand Down
3 changes: 1 addition & 2 deletions dbt/models/marts/dim_contributors.sql
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
-- Type 1 contributor dimension: one row per GitHub login, aggregated across the
-- commit, pull request, and issue staging models. Rebuilt in full each run.
-- Type 1 contributor dimension: one row per GitHub login, rebuilt each run.
-- Stats are commit/PR-based; issue-only authors appear with zeroed counts.

with commits as (
Expand Down
11 changes: 4 additions & 7 deletions dbt/models/marts/dim_repositories.sql
Original file line number Diff line number Diff line change
Expand Up @@ -14,20 +14,17 @@ with versioned as (
topics,
created_at,
updated_at,
-- Full-resolution version timestamp drives the surrogate key so it stays
-- unique even if a repo changes more than once on the same calendar day;
-- valid_from/valid_to are kept at date grain for fact date-range joins.
-- Full-resolution timestamp keys the surrogate so it stays unique across
-- same-day changes; valid_from/valid_to are date grain for fact joins.
dbt_valid_from as version_ts,
-- The earliest version opens at a past-infinity sentinel ('1900-01-01')
-- so historical facts that predate the first snapshot still resolve to
-- the first captured version instead of dropping.
-- Earliest version opens at a past-infinity sentinel so facts predating the
-- first snapshot still resolve to it instead of dropping.
case
when row_number() over (
partition by repository_id order by dbt_valid_from
) = 1 then date '1900-01-01'
else dbt_valid_from::date
end as valid_from,
-- '9999-12-31' = future-infinity bound
coalesce(dbt_valid_to::date, date '9999-12-31') as valid_to,
dbt_valid_to is null as is_current
from {{ ref('snap_repositories') }}
Expand Down
10 changes: 2 additions & 8 deletions dbt/models/marts/fct_commits.sql
Original file line number Diff line number Diff line change
@@ -1,11 +1,5 @@
-- Commit activity fact: one row per commit. Joins to dim_repositories via the SCD2
-- half-open date range (event_date >= valid_from AND < valid_to) so each commit maps
-- to the repository version that was current when it landed; to dim_contributors on
-- author login; and to dim_dates via an inline YYYYMMDD key.
-- Incremental (delete+insert on commit_key): each run only processes rows loaded
-- since the last run, keyed on the monotonic dlt `_loaded_at` (the git author date is
-- not monotonic, so a rebased/old-authored commit pushed today would be skipped).
-- The unique key keeps it idempotent.
-- Commit activity fact: one row per commit, resolving repository via the SCD2 join.
-- Incremental on the dlt `_loaded_at` because the git author date isn't monotonic.

{{
config(
Expand Down
5 changes: 2 additions & 3 deletions dbt/models/marts/fct_contributor_activity_monthly.sql
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
-- Aggregate fact: one row per contributor per month, pivoting the activity event
-- stream into commit / PR / issue counts. Built on int_contributor_activity and the
-- conformed dims (dim_contributors, dim_dates). Foundation for retention analysis.
-- Aggregate fact: one row per contributor per month, pivoting int_contributor_activity
-- into commit / PR / issue counts. Foundation for retention analysis.

with monthly as (
select
Expand Down
9 changes: 3 additions & 6 deletions dbt/models/marts/fct_daily_downloads.sql
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
-- PyPI daily download fact: one row per package per day. `repository_key` is resolved
-- through the `projects` seed (package -> repo) and the SCD2 dim_repositories half-open
-- range; it is nullable for packages with no mapped/ingested repo. Incremental
-- (delete+insert on download_key): only processes days at or after the latest
-- download_date already loaded; the unique key keeps it idempotent.
-- PyPI daily download fact: one row per package per day. `repository_key` resolves
-- through the `projects` seed (package -> repo) and the SCD2 join; nullable for
-- unmapped packages. Incremental (delete+insert on download_key) keeps it idempotent.

{{
config(
Expand All @@ -16,7 +14,6 @@
with downloads as (
select * from {{ ref('stg_pypi__downloads') }}
{% if is_incremental() %}
-- date_key is the only date column on the target table; compare the day's key.
where {{ date_key('download_date') }} >= (select max(date_key) from {{ this }})
{% endif %}
)
Expand Down
5 changes: 2 additions & 3 deletions dbt/models/marts/fct_issues.sql
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
-- Issue lifecycle fact: one row per issue. Repository resolved via the SCD2 half-open
-- range on the issue open date; author via dim_contributors; opened/closed date keys
-- are inline YYYYMMDD references to dim_dates (closed is nullable).
-- Issue lifecycle fact: one row per issue, resolving repository via the SCD2 join on
-- the open date (closed_date_key is nullable for open issues).

with issues as (
select * from {{ ref('stg_github__issues') }}
Expand Down
5 changes: 2 additions & 3 deletions dbt/models/marts/fct_pull_requests.sql
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
-- Pull request lifecycle fact: one row per PR. Repository resolved via the SCD2
-- half-open range on the PR open date; author via dim_contributors; opened/merged
-- date keys are inline YYYYMMDD references to dim_dates (merged is nullable).
-- Pull request lifecycle fact: one row per PR, resolving repository via the SCD2 join
-- on the open date (merged_date_key is nullable for unmerged PRs).

with pull_requests as (
select * from {{ ref('stg_github__pull_requests') }}
Expand Down
10 changes: 3 additions & 7 deletions dbt/models/marts/fct_releases.sql
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
-- Release activity fact: one row per published release. Joins to dim_repositories via
-- the SCD2 half-open date range (event_date >= valid_from AND < valid_to) so each
-- release maps to the repository version current when it was published; to dim_dates
-- via an inline YYYYMMDD key on the publish date. An event fact with no additive
-- measure - analyze by counting (release cadence, time between releases).
-- Incremental (delete+insert on release_key) keyed on the monotonic dlt `_loaded_at`
-- (publish dates can be backdated); the unique key keeps it idempotent.
-- Release activity fact: one row per published release, resolving repository via the
-- SCD2 join on the publish date. No additive measure - analyze by counting (cadence).
-- Incremental on the monotonic dlt `_loaded_at` (publish dates can be backdated).

{{
config(
Expand Down
11 changes: 4 additions & 7 deletions dbt/models/marts/fct_repository_metrics.sql
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
-- GitHub repository metrics fact: a periodic-snapshot fact at one row per repository
-- per capture (ingestion) day, recording the volatile counters stars/forks/open_issues.
-- GitHub's API returns only the *current* counter values (no history), so history is
-- accumulated incrementally - each daily run appends that day's values, dated by the
-- dlt load timestamp. `repository_key` resolves through the SCD2 dim_repositories
-- half-open range for the capture date. Incremental (delete+insert on metric_key)
-- keeps same-day re-runs idempotent.
-- Periodic-snapshot fact: one row per repository per capture day, recording the
-- volatile stars/forks/open_issues. GitHub returns only current values (no history),
-- so each daily run appends that day's snapshot. Incremental (delete+insert on
-- metric_key) keeps same-day re-runs idempotent.

{{
config(
Expand Down
1 change: 0 additions & 1 deletion dbt/models/staging/github/stg_github__commits.sql
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,4 @@ select
c._loaded_at
from commits c
left join parent_counts p on c._dlt_id = p._dlt_parent_id
-- Keep non-merge commits (<= 1 parent); commits with no parents are kept too.
where coalesce(p.parent_count, 0) <= 1
5 changes: 2 additions & 3 deletions dbt/models/staging/github/stg_github__issues.sql
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
-- Structural cleaning over dlt's normalized `issues` table. The issues endpoint
-- returns PRs too; drop them (real issues have no `pull_request`). Labels live in
-- the `issues__labels` child table (see stg_github__issue_labels).
-- Structural cleaning over dlt's normalized `issues` table. The endpoint returns PRs
-- too; drop them (real issues have no `pull_request`). Labels: see stg_github__issue_labels.

with issues as (
select * from {{ source('github', 'issues') }}
Expand Down
4 changes: 2 additions & 2 deletions dbt/models/staging/github/stg_github__repositories.sql
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
-- Structural cleaning over dlt's normalized `repositories` table. Topics are
-- re-aggregated from the dlt child table into a sorted comma string so the SCD2
-- snapshot's check_cols see a stable scalar.
-- re-aggregated from the child table into a sorted comma string so the snapshot's
-- check_cols see a stable scalar.

with repositories as (
select * from {{ source('github', 'repositories') }}
Expand Down
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@ line-length = 88
select = ["E", "W", "F", "I", "UP", "B", "C4", "SIM", "PTH", "RUF"]

[tool.ruff.lint.isort]
# `lib`/`views` are the dashboard app's own packages (imported relative to the
# dashboard/ dir, which Streamlit puts on sys.path), so group them as first-party.
# `lib`/`views` are the dashboard app's own packages (relative to dashboard/ on
# Streamlit's sys.path), so treat them as first-party.
known-first-party = ["lib", "views"]

[tool.pytest.ini_options]
Expand Down
6 changes: 2 additions & 4 deletions src/repolytics/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,8 @@ class Settings(BaseSettings):

@cached_property
def projects(self) -> list[dict[str, str]]:
"""Projects to ingest, each a `{repo, package}` row from `projects_file`.

`package` may be blank for repos not published to PyPI. Returns an empty
list when the file is absent.
"""Projects to ingest as `{repo, package}` rows (empty if the file is absent;
`package` may be blank for repos not on PyPI).
"""
if not self.projects_file.exists():
return []
Expand Down
13 changes: 5 additions & 8 deletions src/repolytics/ingestion/_meta.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,17 @@
"""Shared extraction-metadata stamping for dlt resources.

dlt does not attribute sub-resource rows (commits/issues to their repo) or add a
per-row load timestamp by default, so each resource maps its records through
`stamp(...)` to inject `_loaded_at` plus constant `_`-prefixed provenance columns
(`_repo` for GitHub sub-resources, `_package` for PyPI).
dlt does not attribute sub-resource rows to their repo or add a load timestamp, so
resources map records through `stamp(...)` to inject `_loaded_at` plus constant
`_`-prefixed provenance columns (`_repo`, `_package`).
"""

from collections.abc import Callable
from datetime import UTC, datetime


def stamp(**metadata: str) -> Callable[[dict], dict]:
"""Build a dlt `add_map` function that stamps `_loaded_at` + `metadata`.

Returns a callable applied to each record; it adds a UTC `_loaded_at` and one
constant column per `metadata` key (e.g. `_repo`/`_package`).
"""Build a dlt `add_map` callable that adds a UTC `_loaded_at` and one constant
column per `metadata` key (e.g. `_repo`/`_package`) to each record.
"""

def _apply(record: dict) -> dict:
Expand Down
Loading