Skip to content

Commit bb23ed0

Browse files
Added function descriptions and other notes for better readability and understanding in python files
1 parent e138572 commit bb23ed0

9 files changed

Lines changed: 263 additions & 0 deletions

glue_jobs/build_dim_media.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ class DimMediaError(RuntimeError):
4444

4545
@dataclass(frozen=True)
4646
class JobConfig:
47+
"""Job arguments that control input resolution and Delta output."""
48+
4749
job_name: str
4850
ingestion_run_id: str | None
4951
validation_report_uri: str | None
@@ -55,11 +57,15 @@ class JobConfig:
5557

5658
@dataclass(frozen=True)
5759
class RunInput:
60+
"""The ingestion run and validation report selected for this execution."""
61+
5862
ingestion_run_id: str
5963
validation_report_uri: str
6064

6165

6266
def configure_logging() -> None:
67+
"""Send consistently formatted job messages to CloudWatch Logs."""
68+
6369
logging.basicConfig(
6470
level=logging.INFO,
6571
format="%(asctime)s %(levelname)s %(name)s %(message)s",
@@ -68,6 +74,8 @@ def configure_logging() -> None:
6874

6975

7076
def parse_optional_argument(name: str, default: str | None = None) -> str | None:
77+
"""Read an optional --NAME value from the Glue job command line."""
78+
7179
flag = f"--{name}"
7280
if flag not in sys.argv:
7381
return default
@@ -78,6 +86,8 @@ def parse_optional_argument(name: str, default: str | None = None) -> str | None
7886

7987

8088
def load_config() -> JobConfig:
89+
"""Parse Glue arguments once and return a typed configuration object."""
90+
8191
required = getResolvedOptions(sys.argv, ["JOB_NAME"])
8292
refined_prefix = parse_optional_argument("REFINED_PREFIX", "refined/dim_media")
8393
assert refined_prefix is not None
@@ -93,6 +103,8 @@ def load_config() -> JobConfig:
93103

94104

95105
def workflow_context(config: JobConfig) -> tuple[str, str] | None:
106+
"""Return complete workflow identifiers or fail on a partial configuration."""
107+
96108
if not config.workflow_name and not config.workflow_run_id:
97109
return None
98110
if not config.workflow_name or not config.workflow_run_id:
@@ -101,6 +113,8 @@ def workflow_context(config: JobConfig) -> tuple[str, str] | None:
101113

102114

103115
def resolve_run_input(glue_client: Any, config: JobConfig) -> RunInput:
116+
"""Choose manual inputs first, otherwise read this workflow run's inputs."""
117+
104118
if config.ingestion_run_id or config.validation_report_uri:
105119
if not config.ingestion_run_id or not config.validation_report_uri:
106120
raise DimMediaError(
@@ -144,13 +158,17 @@ def resolve_run_input(glue_client: Any, config: JobConfig) -> RunInput:
144158

145159

146160
def parse_s3_uri(uri: str) -> tuple[str, str]:
161+
"""Split an S3 URI into bucket and key, rejecting malformed locations."""
162+
147163
parsed = urlparse(uri)
148164
if parsed.scheme != "s3" or not parsed.netloc or not parsed.path.lstrip("/"):
149165
raise DimMediaError(f"Invalid S3 URI: {uri!r}.")
150166
return parsed.netloc, parsed.path.lstrip("/")
151167

152168

153169
def read_validation_report(s3_client: Any, uri: str) -> dict[str, Any]:
170+
"""Download and parse the validation report that points to valid raw data."""
171+
154172
bucket, key = parse_s3_uri(uri)
155173
try:
156174
body = s3_client.get_object(Bucket=bucket, Key=key)["Body"].read()
@@ -166,6 +184,8 @@ def resolve_raw_input(
166184
report: dict[str, Any],
167185
expected_ingestion_run_id: str,
168186
) -> str:
187+
"""Verify report lineage and return the validated raw object URI."""
188+
169189
report_run_id = report.get("ingestion_run_id")
170190
if report_run_id != expected_ingestion_run_id:
171191
raise DimMediaError(
@@ -180,6 +200,8 @@ def resolve_raw_input(
180200

181201

182202
def valid_record_count(report: dict[str, Any]) -> int:
203+
"""Read the report's valid count as a non-negative integer."""
204+
183205
count = report.get("valid_record_count")
184206
if type(count) is not int or count < 0:
185207
raise DimMediaError("Validation report has an invalid valid_record_count.")
@@ -190,6 +212,8 @@ def resolve_table_uri(
190212
raw_input_uri: str,
191213
config: JobConfig,
192214
) -> str:
215+
"""Use an explicit Delta location or infer one in the raw bucket."""
216+
193217
if config.dim_media_table_uri:
194218
parse_s3_uri(config.dim_media_table_uri)
195219
return config.dim_media_table_uri.rstrip("/")
@@ -198,6 +222,8 @@ def resolve_table_uri(
198222

199223

200224
def channel_from_title(title: str | None) -> str | None:
225+
"""Classify a media title as Youtube or Facebook when unambiguous."""
226+
201227
if not isinstance(title, str):
202228
return None
203229
lowered = title.lower()
@@ -209,6 +235,8 @@ def channel_from_title(title: str | None) -> str | None:
209235

210236

211237
def build_dimension_dataframe(spark: Any, raw_input_uri: str) -> Any:
238+
"""Create one latest, valid media record per media ID from raw events."""
239+
212240
from pyspark.sql import Window
213241
from pyspark.sql import functions as functions
214242
from pyspark.sql.types import StringType
@@ -244,6 +272,8 @@ def build_dimension_dataframe(spark: Any, raw_input_uri: str) -> Any:
244272
f"Unable to derive a unique Youtube or Facebook channel for: {examples}."
245273
)
246274

275+
# If a media appears multiple times, keep the attributes from its newest
276+
# event so the dimension reflects the latest known title and URL.
247277
latest_per_media = Window.partitionBy("media_id").orderBy(
248278
functions.col("_received_at").desc(),
249279
functions.col("title").desc(),
@@ -257,6 +287,8 @@ def build_dimension_dataframe(spark: Any, raw_input_uri: str) -> Any:
257287

258288

259289
def upsert_delta_table(spark: Any, dimension: Any, table_uri: str) -> int:
290+
"""Insert new media and update existing media in the Delta table."""
291+
260292
from delta.tables import DeltaTable
261293

262294
row_count = dimension.count()
@@ -288,6 +320,8 @@ def publish_workflow_properties(
288320
table_uri: str,
289321
row_count: int,
290322
) -> None:
323+
"""Publish the table URI and row count for later workflow jobs."""
324+
291325
context = workflow_context(config)
292326
if context is None:
293327
LOGGER.info("No Glue workflow context found; skipping dim_media property publication.")
@@ -312,6 +346,8 @@ def publish_workflow_properties(
312346

313347

314348
def main() -> None:
349+
"""Resolve validated input, build dim_media, and upsert it into Delta."""
350+
315351
from awsglue.context import GlueContext
316352
from awsglue.job import Job
317353
from pyspark.context import SparkContext

glue_jobs/build_dim_visitors.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ class DimVisitorsError(RuntimeError):
4444

4545
@dataclass(frozen=True)
4646
class JobConfig:
47+
"""Job arguments that control input resolution and Delta output."""
48+
4749
job_name: str
4850
ingestion_run_id: str | None
4951
validation_report_uri: str | None
@@ -55,11 +57,15 @@ class JobConfig:
5557

5658
@dataclass(frozen=True)
5759
class RunInput:
60+
"""The ingestion run and validation report selected for this execution."""
61+
5862
ingestion_run_id: str
5963
validation_report_uri: str
6064

6165

6266
def configure_logging() -> None:
67+
"""Send consistently formatted job messages to CloudWatch Logs."""
68+
6369
logging.basicConfig(
6470
level=logging.INFO,
6571
format="%(asctime)s %(levelname)s %(name)s %(message)s",
@@ -68,6 +74,8 @@ def configure_logging() -> None:
6874

6975

7076
def parse_optional_argument(name: str, default: str | None = None) -> str | None:
77+
"""Read an optional --NAME value from the Glue job command line."""
78+
7179
flag = f"--{name}"
7280
if flag not in sys.argv:
7381
return default
@@ -78,6 +86,8 @@ def parse_optional_argument(name: str, default: str | None = None) -> str | None
7886

7987

8088
def load_config() -> JobConfig:
89+
"""Parse Glue arguments once and return a typed configuration object."""
90+
8191
required = getResolvedOptions(sys.argv, ["JOB_NAME"])
8292
refined_prefix = parse_optional_argument("REFINED_PREFIX", "refined/dim_visitors")
8393
assert refined_prefix is not None
@@ -93,6 +103,8 @@ def load_config() -> JobConfig:
93103

94104

95105
def workflow_context(config: JobConfig) -> tuple[str, str] | None:
106+
"""Return complete workflow identifiers or fail on a partial configuration."""
107+
96108
if not config.workflow_name and not config.workflow_run_id:
97109
return None
98110
if not config.workflow_name or not config.workflow_run_id:
@@ -101,6 +113,8 @@ def workflow_context(config: JobConfig) -> tuple[str, str] | None:
101113

102114

103115
def resolve_run_input(glue_client: Any, config: JobConfig) -> RunInput:
116+
"""Choose manual inputs first, otherwise read this workflow run's inputs."""
117+
104118
if config.ingestion_run_id or config.validation_report_uri:
105119
if not config.ingestion_run_id or not config.validation_report_uri:
106120
raise DimVisitorsError(
@@ -144,13 +158,17 @@ def resolve_run_input(glue_client: Any, config: JobConfig) -> RunInput:
144158

145159

146160
def parse_s3_uri(uri: str) -> tuple[str, str]:
161+
"""Split an S3 URI into bucket and key, rejecting malformed locations."""
162+
147163
parsed = urlparse(uri)
148164
if parsed.scheme != "s3" or not parsed.netloc or not parsed.path.lstrip("/"):
149165
raise DimVisitorsError(f"Invalid S3 URI: {uri!r}.")
150166
return parsed.netloc, parsed.path.lstrip("/")
151167

152168

153169
def read_validation_report(s3_client: Any, uri: str) -> dict[str, Any]:
170+
"""Download and parse the validation report that points to valid raw data."""
171+
154172
bucket, key = parse_s3_uri(uri)
155173
try:
156174
body = s3_client.get_object(Bucket=bucket, Key=key)["Body"].read()
@@ -166,6 +184,8 @@ def resolve_raw_input(
166184
report: dict[str, Any],
167185
expected_ingestion_run_id: str,
168186
) -> str:
187+
"""Verify report lineage and return the validated raw object URI."""
188+
169189
report_run_id = report.get("ingestion_run_id")
170190
if report_run_id != expected_ingestion_run_id:
171191
raise DimVisitorsError(
@@ -180,13 +200,17 @@ def resolve_raw_input(
180200

181201

182202
def valid_record_count(report: dict[str, Any]) -> int:
203+
"""Read the report's valid count as a non-negative integer."""
204+
183205
count = report.get("valid_record_count")
184206
if type(count) is not int or count < 0:
185207
raise DimVisitorsError("Validation report has an invalid valid_record_count.")
186208
return count
187209

188210

189211
def resolve_table_uri(raw_input_uri: str, config: JobConfig) -> str:
212+
"""Use an explicit Delta location or infer one in the raw bucket."""
213+
190214
if config.dim_visitors_table_uri:
191215
parse_s3_uri(config.dim_visitors_table_uri)
192216
return config.dim_visitors_table_uri.rstrip("/")
@@ -195,6 +219,8 @@ def resolve_table_uri(raw_input_uri: str, config: JobConfig) -> str:
195219

196220

197221
def build_dimension_dataframe(spark: Any, raw_input_uri: str) -> Any:
222+
"""Create one latest visitor record per visitor ID from raw events."""
223+
198224
from pyspark.sql import Window
199225
from pyspark.sql import functions as functions
200226

@@ -212,6 +238,8 @@ def build_dimension_dataframe(spark: Any, raw_input_uri: str) -> Any:
212238
functions.col("country"),
213239
functions.to_timestamp("received_at").alias("_received_at"),
214240
)
241+
# A visitor may have many events. The newest event supplies the current
242+
# IP address and country stored in the dimension.
215243
latest_per_visitor = Window.partitionBy("visitor_id").orderBy(
216244
functions.col("_received_at").desc(),
217245
functions.col("ip_address").desc(),
@@ -228,6 +256,8 @@ def build_dimension_dataframe(spark: Any, raw_input_uri: str) -> Any:
228256

229257

230258
def upsert_delta_table(spark: Any, dimension: Any, table_uri: str) -> int:
259+
"""Insert new visitors and update existing visitors in the Delta table."""
260+
231261
from delta.tables import DeltaTable
232262

233263
row_count = dimension.count()
@@ -259,6 +289,8 @@ def publish_workflow_properties(
259289
table_uri: str,
260290
row_count: int,
261291
) -> None:
292+
"""Publish the table URI and row count for later workflow jobs."""
293+
262294
context = workflow_context(config)
263295
if context is None:
264296
LOGGER.info(
@@ -285,6 +317,8 @@ def publish_workflow_properties(
285317

286318

287319
def main() -> None:
320+
"""Resolve validated input, build dim_visitors, and upsert it into Delta."""
321+
288322
from awsglue.context import GlueContext
289323
from awsglue.job import Job
290324
from pyspark.context import SparkContext

0 commit comments

Comments
 (0)