From 28d74ad438f6026983da2d61f8a00e0e2dae0d62 Mon Sep 17 00:00:00 2001 From: Christian Sanchez Date: Fri, 6 Feb 2026 08:35:54 -0800 Subject: [PATCH 01/13] updating version numbers --- .github/workflows/ci.yml | 2 +- .goreleaser.yml | 2 +- Dockerfile | 2 +- README.md | 6 +++--- card_data/pipelines/poke_cli_dbt/dbt_project.yml | 2 +- nfpm.yaml | 2 +- testdata/main_latest_flag.golden | 2 +- 7 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1bc18d1..8ff0f94 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,7 +31,7 @@ on: - main env: - VERSION_NUMBER: 'v1.8.8' + VERSION_NUMBER: 'v1.8.9' DOCKERHUB_REGISTRY_NAME: 'digitalghostdev/poke-cli' AWS_REGION: 'us-west-2' diff --git a/.goreleaser.yml b/.goreleaser.yml index 17682f0..e2944c1 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -14,7 +14,7 @@ builds: - windows - darwin ldflags: - - -s -w -X main.version=v1.8.8 + - -s -w -X main.version=v1.8.9 archives: - formats: [ 'zip' ] diff --git a/Dockerfile b/Dockerfile index 5aaa1e3..7261655 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,7 +8,7 @@ RUN go mod download COPY . . -RUN go build -ldflags "-X main.version=v1.8.8" -o poke-cli . +RUN go build -ldflags "-X main.version=v1.8.9" -o poke-cli . # build 2 FROM --platform=$BUILDPLATFORM alpine:3.23 diff --git a/README.md b/README.md index e1751aa..6acac16 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ pokemon-logo

version-label - docker-image-size + docker-image-size ci-status-badge
@@ -96,11 +96,11 @@ Cloudsmith is a fully cloud-based service that lets you easily create, store, an 3. Choose how to interact with the container: * Run a single command and exit: ```bash - docker run --rm -it digitalghostdev/poke-cli:v1.8.8 [subcommand] [flag] + docker run --rm -it digitalghostdev/poke-cli:v1.8.9 [subcommand] [flag] ``` * Enter the container and use its shell: ```bash - docker run --rm -it --name poke-cli --entrypoint /bin/sh digitalghostdev/poke-cli:v1.8.8 -c "cd /app && exec sh" + docker run --rm -it --name poke-cli --entrypoint /bin/sh digitalghostdev/poke-cli:v1.8.9 -c "cd /app && exec sh" # placed into the /app directory, run the program with './poke-cli' # example: ./poke-cli ability swift-swim ``` diff --git a/card_data/pipelines/poke_cli_dbt/dbt_project.yml b/card_data/pipelines/poke_cli_dbt/dbt_project.yml index ee355c7..ace000b 100644 --- a/card_data/pipelines/poke_cli_dbt/dbt_project.yml +++ b/card_data/pipelines/poke_cli_dbt/dbt_project.yml @@ -1,5 +1,5 @@ name: 'poke_cli_dbt' -version: '1.8.8' +version: '1.8.9' profile: 'poke_cli_dbt' diff --git a/nfpm.yaml b/nfpm.yaml index e2fded1..32cf979 100644 --- a/nfpm.yaml +++ b/nfpm.yaml @@ -1,7 +1,7 @@ name: "poke-cli" arch: "arm64" platform: "linux" -version: "v1.8.8" +version: "v1.8.9" section: "default" version_schema: semver maintainer: "Christian S" diff --git a/testdata/main_latest_flag.golden b/testdata/main_latest_flag.golden index 306c19b..1b82be8 100644 --- a/testdata/main_latest_flag.golden +++ b/testdata/main_latest_flag.golden @@ -2,6 +2,6 @@ ┃ ┃ ┃ Latest available release ┃ ┃ on GitHub: ┃ -┃ • v1.8.7 ┃ +┃ • v1.8.8 ┃ ┃ ┃ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ From 71085410fab96a0bbd2bfb45c60e9492bc5e7fbe Mon Sep 17 00:00:00 2001 From: Christian Sanchez Date: Fri, 6 Feb 2026 09:13:22 -0800 Subject: [PATCH 02/13] updating job names --- card_data/pipelines/definitions.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/card_data/pipelines/definitions.py b/card_data/pipelines/definitions.py index e1c9c47..e0344d0 100644 --- a/card_data/pipelines/definitions.py +++ b/card_data/pipelines/definitions.py @@ -24,8 +24,8 @@ def defs() -> dg.Definitions: sensors=[discord_success_sensor, discord_failure_sensor], ) -# Pricing pipeline -pricing_pipeline_job = dg.define_asset_job( +# Pricing pipeline job +pricing_pipeline = dg.define_asset_job( name="pricing_pipeline_job", selection=dg.AssetSelection.assets(build_dataframe).downstream(include_self=True), ) @@ -33,34 +33,34 @@ def defs() -> dg.Definitions: price_schedule: dg.ScheduleDefinition = dg.ScheduleDefinition( name="price_schedule", cron_schedule="0 14 * * *", - target=pricing_pipeline_job, + target=pricing_pipeline, execution_timezone="America/Los_Angeles", ) defs_pricing: dg.Definitions = dg.Definitions( assets=[build_dataframe, load_pricing_data, data_quality_checks_on_pricing], - jobs=[pricing_pipeline_job], + jobs=[pricing_pipeline], schedules=[price_schedule], ) -# Series pipeline -series_pipeline_job = dg.define_asset_job( +# Series pipeline job +series_pipeline = dg.define_asset_job( name="series_pipeline_job", selection=dg.AssetSelection.assets(extract_series_data).downstream(include_self=True), ) defs_series: dg.Definitions = dg.Definitions( assets=[extract_series_data, load_series_data, data_quality_check_on_series], - jobs=[series_pipeline_job], + jobs=[series_pipeline], ) -# Sets pipeline -sets_pipeline_job = dg.define_asset_job( +# Sets pipeline job +sets_pipeline = dg.define_asset_job( name="sets_pipeline_job", selection=dg.AssetSelection.assets(extract_sets_data).downstream(include_self=True), ) defs_sets: dg.Definitions = dg.Definitions( assets=[extract_sets_data, load_sets_data, data_quality_check_on_sets], - jobs=[sets_pipeline_job], + jobs=[sets_pipeline], ) \ No newline at end of file From b95d59933a64811a4c7a7f75b94319d9ea0b6977 Mon Sep 17 00:00:00 2001 From: Christian Sanchez Date: Fri, 6 Feb 2026 09:16:23 -0800 Subject: [PATCH 03/13] adding ascended heroes set (#241) --- .../defs/extract/tcgcsv/extract_pricing.py | 22 ++++++++++++++++--- .../defs/extract/tcgdex/extract_cards.py | 2 +- card_data/pipelines/soda/checks_sets.yml | 2 +- card_data/uv.lock | 4 ++-- 4 files changed, 23 insertions(+), 7 deletions(-) diff --git a/card_data/pipelines/defs/extract/tcgcsv/extract_pricing.py b/card_data/pipelines/defs/extract/tcgcsv/extract_pricing.py index 101b6b3..c80df0e 100644 --- a/card_data/pipelines/defs/extract/tcgcsv/extract_pricing.py +++ b/card_data/pipelines/defs/extract/tcgcsv/extract_pricing.py @@ -10,6 +10,8 @@ SET_PRODUCT_MATCHING = { + # Mega Evolution + "me02.5": "24541", "me02": "24448", "me01": "24380", # Scarlet & Violet @@ -57,7 +59,7 @@ class CardPricing(BaseModel): def is_card(item: dict) -> bool: - """Check if item has a 'Number' field in extendedData""" + """Check if an item has a 'Number' field in extendedData""" return any( data_field.get("name") == "Number" for data_field in item.get("extendedData", []) @@ -126,9 +128,23 @@ def pull_product_information(set_number: str) -> pl.DataFrame: if not is_card(card): continue - # Skip ball pattern variants (unique to Prismatic Evolutions) + # Skip cosmetic holofoil pattern variants. + # Prismatic Evolutions (SV08.5) uses Poke Ball / Master Ball patterns. + # Ascended Heroes (ME2.5) uses ball-type and energy symbol patterns. card_name = card.get("name", "") - if "(Poke Ball Pattern)" in card_name or "(Master Ball Pattern)" in card_name: + skip_variants = [ + "(Poke Ball Pattern)", + "(Master Ball Pattern)", + "(Love Ball)", + "(Energy Symbol Pattern)", + "(Poke Ball)", + "(Dusk Ball)", + "(Quick Ball)", + "(Friend Ball)", + "(Team Rocket)", + "(Exclusive)", + ] + if any(variant in card_name for variant in skip_variants): continue card_info = { diff --git a/card_data/pipelines/defs/extract/tcgdex/extract_cards.py b/card_data/pipelines/defs/extract/tcgdex/extract_cards.py index cf6f350..21fb2cb 100644 --- a/card_data/pipelines/defs/extract/tcgdex/extract_cards.py +++ b/card_data/pipelines/defs/extract/tcgdex/extract_cards.py @@ -10,7 +10,7 @@ @dg.asset(kinds={"API"}, name="extract_card_url_from_set_data") def extract_card_url_from_set() -> list: - urls = ["https://api.tcgdex.net/v2/en/sets/me02"] + urls = ["https://api.tcgdex.net/v2/en/sets/me02.5"] all_card_urls = [] diff --git a/card_data/pipelines/soda/checks_sets.yml b/card_data/pipelines/soda/checks_sets.yml index 49f330d..9eab0e7 100644 --- a/card_data/pipelines/soda/checks_sets.yml +++ b/card_data/pipelines/soda/checks_sets.yml @@ -1,6 +1,6 @@ checks for sets: # Row count validation - - row_count = 39 + - row_count = 40 # Schema validation checks - schema: diff --git a/card_data/uv.lock b/card_data/uv.lock index 39d1cf6..6362426 100644 --- a/card_data/uv.lock +++ b/card_data/uv.lock @@ -199,8 +199,8 @@ requires-dist = [ dev = [ { name = "dagster-dbt", specifier = ">=0.27.3" }, { name = "dagster-dg-cli" }, - { name = "dagster-postgres", specifier = ">=0.27.3" }, - { name = "dagster-webserver" }, + { name = "dagster-postgres", specifier = "==0.28.7" }, + { name = "dagster-webserver", specifier = "==1.12.7" }, { name = "pytest", specifier = ">=9.0.2" }, { name = "pytest-codspeed", specifier = ">=4.2.0" }, { name = "responses", specifier = ">=0.25.8" }, From 274702e512495bcf20e1b69ae43fe7be54f4562b Mon Sep 17 00:00:00 2001 From: Christian Sanchez Date: Fri, 6 Feb 2026 09:17:43 -0800 Subject: [PATCH 04/13] adding a note on pricing --- cmd/card/cardlist.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/card/cardlist.go b/cmd/card/cardlist.go index 0988372..f168318 100644 --- a/cmd/card/cardlist.go +++ b/cmd/card/cardlist.go @@ -295,7 +295,7 @@ func (m CardsModel) View() string { screen := lipgloss.JoinHorizontal(lipgloss.Top, leftPanel, rightPanel) return fmt.Sprintf( - "Highlight a card!\n%s\n%s", + "Highlight a card!\n\nNote: Prices are for normal variations of cards.\n%s\n%s", screen, styling.KeyMenu.Render("↑ (move up)\n↓ (move down)\n? (view image)\ntab (toggle search)\nctrl+c | esc (quit)")) } From e01b79b0c77580b58dd2c801ce7c2e1356f28713 Mon Sep 17 00:00:00 2001 From: Christian Sanchez Date: Fri, 6 Feb 2026 09:22:11 -0800 Subject: [PATCH 05/13] applying `go fmt` fixes --- cmd/card/cardlist_test.go | 1 - cmd/card/imageviewer.go | 2 +- cmd/card/setslist.go | 1 - cmd/move/move.go | 2 +- connections/connection.go | 2 +- 5 files changed, 3 insertions(+), 5 deletions(-) diff --git a/cmd/card/cardlist_test.go b/cmd/card/cardlist_test.go index b4dd5cc..6da8922 100644 --- a/cmd/card/cardlist_test.go +++ b/cmd/card/cardlist_test.go @@ -394,4 +394,3 @@ func TestCardDataMsg_EmptyResult(t *testing.T) { t.Errorf("expected empty maps, got price:%d illus:%d image:%d", len(resultModel.PriceMap), len(resultModel.IllustratorMap), len(resultModel.ImageMap)) } } - diff --git a/cmd/card/imageviewer.go b/cmd/card/imageviewer.go index 0eaed38..68e96ae 100644 --- a/cmd/card/imageviewer.go +++ b/cmd/card/imageviewer.go @@ -32,7 +32,7 @@ func fetchImageCmd(imageURL string) tea.Cmd { } return imageReadyMsg{ imageData: imageData, - protocol: protocol, + protocol: protocol, } } } diff --git a/cmd/card/setslist.go b/cmd/card/setslist.go index 69afbaf..2c328a3 100644 --- a/cmd/card/setslist.go +++ b/cmd/card/setslist.go @@ -169,4 +169,3 @@ func SetsList(seriesID string) (SetsModel, error) { Spinner: s, }, nil } - diff --git a/cmd/move/move.go b/cmd/move/move.go index 0ddd236..37db39e 100644 --- a/cmd/move/move.go +++ b/cmd/move/move.go @@ -97,7 +97,7 @@ func moveInfoContainer(output *strings.Builder, moveStruct structs.MoveJSONStruc func moveEffectContainer(output *strings.Builder, moveStruct structs.MoveJSONStruct) { var sv string - var swsh string + var swsh string docStyle := lipgloss.NewStyle(). Padding(1, 2). diff --git a/connections/connection.go b/connections/connection.go index 3c35ffb..9a3242d 100644 --- a/connections/connection.go +++ b/connections/connection.go @@ -130,4 +130,4 @@ func CallTCGData(url string) ([]byte, error) { } return body, nil -} \ No newline at end of file +} From a6d9a28ff733520a060ac0c208986f13eff4404d Mon Sep 17 00:00:00 2001 From: Christian Sanchez Date: Fri, 6 Feb 2026 13:48:55 -0800 Subject: [PATCH 06/13] adding more Python tests (#216) --- .../pipelines/tests/json_retriever_test.py | 158 +++++++++++++++++ .../pipelines/tests/secret_retriever_test.py | 166 ++++++++++++++++++ 2 files changed, 324 insertions(+) create mode 100644 card_data/pipelines/tests/json_retriever_test.py create mode 100644 card_data/pipelines/tests/secret_retriever_test.py diff --git a/card_data/pipelines/tests/json_retriever_test.py b/card_data/pipelines/tests/json_retriever_test.py new file mode 100644 index 0000000..22ea412 --- /dev/null +++ b/card_data/pipelines/tests/json_retriever_test.py @@ -0,0 +1,158 @@ +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).parent.parent.parent)) + +import pytest +import responses +import requests +from pipelines.utils.json_retriever import fetch_json + + +@pytest.mark.benchmark +@responses.activate +def test_fetch_json_success(): + """Test successful JSON retrieval.""" + responses.add( + responses.GET, + "https://api.example.com/data", + json={"id": 1, "name": "Pikachu"}, + status=200, + ) + + result = fetch_json("https://api.example.com/data") + + assert isinstance(result, dict) # nosec + assert result["id"] == 1 # nosec + assert result["name"] == "Pikachu" # nosec + + +@pytest.mark.benchmark +@responses.activate +def test_fetch_json_with_nested_data(): + """Test retrieval of nested JSON structures.""" + payload = { + "results": [ + {"productId": 100, "name": "Card A"}, + {"productId": 200, "name": "Card B"}, + ], + "totalItems": 2, + } + responses.add( + responses.GET, + "https://api.example.com/products", + json=payload, + status=200, + ) + + result = fetch_json("https://api.example.com/products") + + assert result["totalItems"] == 2 # nosec + assert len(result["results"]) == 2 # nosec + assert result["results"][0]["productId"] == 100 # nosec + + +@pytest.mark.benchmark +@responses.activate +def test_fetch_json_http_404(): + """Test that a 404 response raises HTTPError.""" + responses.add( + responses.GET, + "https://api.example.com/missing", + json={"error": "not found"}, + status=404, + ) + + with pytest.raises(requests.exceptions.HTTPError): + fetch_json("https://api.example.com/missing") + + +@pytest.mark.benchmark +@responses.activate +def test_fetch_json_http_500(): + """Test that a 500 response raises HTTPError.""" + responses.add( + responses.GET, + "https://api.example.com/error", + json={"error": "internal server error"}, + status=500, + ) + + with pytest.raises(requests.exceptions.HTTPError): + fetch_json("https://api.example.com/error") + + +@pytest.mark.benchmark +@responses.activate +def test_fetch_json_connection_error(): + """Test that a connection error raises ConnectionError.""" + responses.add( + responses.GET, + "https://api.example.com/down", + body=requests.exceptions.ConnectionError("Connection refused"), + ) + + with pytest.raises(requests.exceptions.ConnectionError): + fetch_json("https://api.example.com/down") + + +@pytest.mark.benchmark +@responses.activate +def test_fetch_json_timeout(): + """Test that a timeout raises an appropriate exception.""" + responses.add( + responses.GET, + "https://api.example.com/slow", + body=requests.exceptions.ReadTimeout("Read timed out"), + ) + + with pytest.raises(requests.exceptions.ReadTimeout): + fetch_json("https://api.example.com/slow") + + +@pytest.mark.benchmark +@responses.activate +def test_fetch_json_custom_timeout(): + """Test that the custom timeout parameter is passed through.""" + responses.add( + responses.GET, + "https://api.example.com/data", + json={"ok": True}, + status=200, + ) + + result = fetch_json("https://api.example.com/data", timeout=5) + + assert result["ok"] is True # nosec + + +@pytest.mark.benchmark +@responses.activate +def test_fetch_json_empty_object(): + """Test retrieval of an empty JSON object.""" + responses.add( + responses.GET, + "https://api.example.com/empty", + json={}, + status=200, + ) + + result = fetch_json("https://api.example.com/empty") + + assert result == {} # nosec + + +@pytest.mark.benchmark +@responses.activate +def test_fetch_json_invalid_json(): + """Test that an invalid JSON body raises a ValueError (JSONDecodeError).""" + responses.add( + responses.GET, + "https://api.example.com/bad", + body="not valid json {{{", + status=200, + content_type="application/json", + ) + + with pytest.raises(requests.exceptions.JSONDecodeError): + fetch_json("https://api.example.com/bad") diff --git a/card_data/pipelines/tests/secret_retriever_test.py b/card_data/pipelines/tests/secret_retriever_test.py new file mode 100644 index 0000000..1d5672d --- /dev/null +++ b/card_data/pipelines/tests/secret_retriever_test.py @@ -0,0 +1,166 @@ +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).parent.parent.parent)) + +import json +import pytest +from unittest.mock import patch, MagicMock +from pipelines.utils.secret_retriever import fetch_secret, fetch_n8n_webhook_secret + +@pytest.mark.benchmark +@patch("pipelines.utils.secret_retriever.SecretCache") +@patch("pipelines.utils.secret_retriever.botocore.session.get_session") +def test_fetch_secret_success(mock_get_session, mock_secret_cache_cls): + """Test successful retrieval of the Supabase database URI.""" + secret_payload = json.dumps({"database_uri": "postgresql://user:pass@host/db"}) + + mock_cache_instance = MagicMock() + mock_cache_instance.get_secret_string.return_value = secret_payload + mock_secret_cache_cls.return_value = mock_cache_instance + + result = fetch_secret() + + assert result == "postgresql://user:pass@host/db" # nosec + mock_cache_instance.get_secret_string.assert_called_once_with("supabase") + + +@pytest.mark.benchmark +@patch("pipelines.utils.secret_retriever.SecretCache") +@patch("pipelines.utils.secret_retriever.botocore.session.get_session") +def test_fetch_secret_missing_key(mock_get_session, mock_secret_cache_cls): + """Test KeyError when the secret JSON is missing 'database_uri'.""" + secret_payload = json.dumps({"some_other_key": "value"}) + + mock_cache_instance = MagicMock() + mock_cache_instance.get_secret_string.return_value = secret_payload + mock_secret_cache_cls.return_value = mock_cache_instance + + with pytest.raises(KeyError, match="database_uri"): + fetch_secret() + + +@pytest.mark.benchmark +@patch("pipelines.utils.secret_retriever.SecretCache") +@patch("pipelines.utils.secret_retriever.botocore.session.get_session") +def test_fetch_secret_invalid_json(mock_get_session, mock_secret_cache_cls): + """Test that invalid JSON in the secret raises JSONDecodeError.""" + mock_cache_instance = MagicMock() + mock_cache_instance.get_secret_string.return_value = "not valid json" + mock_secret_cache_cls.return_value = mock_cache_instance + + with pytest.raises(json.JSONDecodeError): + fetch_secret() + + +@pytest.mark.benchmark +@patch("pipelines.utils.secret_retriever.SecretCache") +@patch("pipelines.utils.secret_retriever.botocore.session.get_session") +def test_fetch_secret_empty_json_object(mock_get_session, mock_secret_cache_cls): + """Test KeyError when the secret is an empty JSON object.""" + mock_cache_instance = MagicMock() + mock_cache_instance.get_secret_string.return_value = "{}" + mock_secret_cache_cls.return_value = mock_cache_instance + + with pytest.raises(KeyError, match="database_uri"): + fetch_secret() + + +@pytest.mark.benchmark +@patch("pipelines.utils.secret_retriever.SecretCache") +@patch("pipelines.utils.secret_retriever.botocore.session.get_session") +def test_fetch_secret_cache_raises(mock_get_session, mock_secret_cache_cls): + """Test that an exception from SecretCache propagates.""" + mock_cache_instance = MagicMock() + mock_cache_instance.get_secret_string.side_effect = Exception( + "Secret not found" + ) + mock_secret_cache_cls.return_value = mock_cache_instance + + with pytest.raises(Exception, match="Secret not found"): + fetch_secret() + + +# --------------------------------------------------------------------------- +# fetch_n8n_webhook_secret() +# --------------------------------------------------------------------------- + + +@pytest.mark.benchmark +@patch("pipelines.utils.secret_retriever.SecretCache") +@patch("pipelines.utils.secret_retriever.botocore.session.get_session") +def test_fetch_n8n_webhook_secret_success(mock_get_session, mock_secret_cache_cls): + """Test successful retrieval of the n8n webhook URL.""" + secret_payload = json.dumps({"n8n_webhook": "https://n8n.example.com/hook/abc"}) + + mock_cache_instance = MagicMock() + mock_cache_instance.get_secret_string.return_value = secret_payload + mock_secret_cache_cls.return_value = mock_cache_instance + + result = fetch_n8n_webhook_secret() + + assert result == "https://n8n.example.com/hook/abc" # nosec + mock_cache_instance.get_secret_string.assert_called_once_with("n8n_webhook") + + +@pytest.mark.benchmark +@patch("pipelines.utils.secret_retriever.SecretCache") +@patch("pipelines.utils.secret_retriever.botocore.session.get_session") +def test_fetch_n8n_webhook_secret_missing_key(mock_get_session, mock_secret_cache_cls): + """Test KeyError when the secret JSON is missing 'n8n_webhook'.""" + secret_payload = json.dumps({"wrong_key": "value"}) + + mock_cache_instance = MagicMock() + mock_cache_instance.get_secret_string.return_value = secret_payload + mock_secret_cache_cls.return_value = mock_cache_instance + + with pytest.raises(KeyError, match="n8n_webhook"): + fetch_n8n_webhook_secret() + + +@pytest.mark.benchmark +@patch("pipelines.utils.secret_retriever.SecretCache") +@patch("pipelines.utils.secret_retriever.botocore.session.get_session") +def test_fetch_n8n_webhook_secret_invalid_json( + mock_get_session, mock_secret_cache_cls +): + """Test that invalid JSON in the secret raises JSONDecodeError.""" + mock_cache_instance = MagicMock() + mock_cache_instance.get_secret_string.return_value = "{broken" + mock_secret_cache_cls.return_value = mock_cache_instance + + with pytest.raises(json.JSONDecodeError): + fetch_n8n_webhook_secret() + + +@pytest.mark.benchmark +@patch("pipelines.utils.secret_retriever.SecretCache") +@patch("pipelines.utils.secret_retriever.botocore.session.get_session") +def test_fetch_n8n_webhook_secret_empty_json_object( + mock_get_session, mock_secret_cache_cls +): + """Test KeyError when the secret is an empty JSON object.""" + mock_cache_instance = MagicMock() + mock_cache_instance.get_secret_string.return_value = "{}" + mock_secret_cache_cls.return_value = mock_cache_instance + + with pytest.raises(KeyError, match="n8n_webhook"): + fetch_n8n_webhook_secret() + + +@pytest.mark.benchmark +@patch("pipelines.utils.secret_retriever.SecretCache") +@patch("pipelines.utils.secret_retriever.botocore.session.get_session") +def test_fetch_n8n_webhook_secret_cache_raises( + mock_get_session, mock_secret_cache_cls +): + """Test that an exception from SecretCache propagates.""" + mock_cache_instance = MagicMock() + mock_cache_instance.get_secret_string.side_effect = Exception( + "Access denied" + ) + mock_secret_cache_cls.return_value = mock_cache_instance + + with pytest.raises(Exception, match="Access denied"): + fetch_n8n_webhook_secret() + \ No newline at end of file From 9136397f6b5ddd119ddb01d91c8c5e4f339748de Mon Sep 17 00:00:00 2001 From: Christian Sanchez Date: Fri, 6 Feb 2026 14:39:13 -0800 Subject: [PATCH 07/13] initial commit --- .github/workflows/python_lint.yml | 35 +++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 .github/workflows/python_lint.yml diff --git a/.github/workflows/python_lint.yml b/.github/workflows/python_lint.yml new file mode 100644 index 0000000..d7e3741 --- /dev/null +++ b/.github/workflows/python_lint.yml @@ -0,0 +1,35 @@ +name: Python Lint + +on: + pull_request: + types: [opened, reopened, synchronize] + paths: + - 'card_data/**' + +permissions: + contents: read + +jobs: + ruff: + runs-on: ubuntu-22.04 + defaults: + run: + working-directory: card_data + + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Setup Python + uses: actions/setup-python@v6 + with: + python-version: '3.12' + + - name: Install uv + uses: astral-sh/setup-uv@v7 + + - name: Install ruff + run: uv tool install ruff + + - name: Lint + run: ruff check pipelines/ From 292da5a448379e700c2aa9abe7036263696eab3f Mon Sep 17 00:00:00 2001 From: Christian Sanchez Date: Fri, 6 Feb 2026 14:39:33 -0800 Subject: [PATCH 08/13] removing unused import --- card_data/pipelines/defs/extract/tcgdex/extract_sets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/card_data/pipelines/defs/extract/tcgdex/extract_sets.py b/card_data/pipelines/defs/extract/tcgdex/extract_sets.py index 5c635e2..0de00a2 100644 --- a/card_data/pipelines/defs/extract/tcgdex/extract_sets.py +++ b/card_data/pipelines/defs/extract/tcgdex/extract_sets.py @@ -2,7 +2,7 @@ import dagster as dg import polars as pl -from pydantic import BaseModel, HttpUrl, ValidationError +from pydantic import BaseModel, ValidationError from termcolor import colored from ....utils.json_retriever import fetch_json From 6d8f6f28915a59629373a9ac0e24f24402e11b92 Mon Sep 17 00:00:00 2001 From: Christian Sanchez Date: Fri, 6 Feb 2026 15:04:02 -0800 Subject: [PATCH 09/13] updating tests (#216) --- card_data/pipelines/tests/json_retriever_test.py | 16 ---------------- .../pipelines/tests/secret_retriever_test.py | 1 - 2 files changed, 17 deletions(-) diff --git a/card_data/pipelines/tests/json_retriever_test.py b/card_data/pipelines/tests/json_retriever_test.py index 22ea412..316da8f 100644 --- a/card_data/pipelines/tests/json_retriever_test.py +++ b/card_data/pipelines/tests/json_retriever_test.py @@ -110,22 +110,6 @@ def test_fetch_json_timeout(): fetch_json("https://api.example.com/slow") -@pytest.mark.benchmark -@responses.activate -def test_fetch_json_custom_timeout(): - """Test that the custom timeout parameter is passed through.""" - responses.add( - responses.GET, - "https://api.example.com/data", - json={"ok": True}, - status=200, - ) - - result = fetch_json("https://api.example.com/data", timeout=5) - - assert result["ok"] is True # nosec - - @pytest.mark.benchmark @responses.activate def test_fetch_json_empty_object(): diff --git a/card_data/pipelines/tests/secret_retriever_test.py b/card_data/pipelines/tests/secret_retriever_test.py index 1d5672d..4ba9f92 100644 --- a/card_data/pipelines/tests/secret_retriever_test.py +++ b/card_data/pipelines/tests/secret_retriever_test.py @@ -163,4 +163,3 @@ def test_fetch_n8n_webhook_secret_cache_raises( with pytest.raises(Exception, match="Access denied"): fetch_n8n_webhook_secret() - \ No newline at end of file From e837aa19bd8f7d2d8719cf1f7b2ee35a5f21e23e Mon Sep 17 00:00:00 2001 From: Christian Sanchez Date: Fri, 6 Feb 2026 15:04:41 -0800 Subject: [PATCH 10/13] updating TCG demo gif --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6acac16..e68681a 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ View the [documentation](https://docs.poke-cli.com) on the data infrastructure i ### Trading Card Game Data -![demo-tcg](https://poke-cli-s3-bucket.s3.us-west-2.amazonaws.com/poke-cli-v1.8.0.gif) +![demo-tcg](https://poke-cli-s3-bucket.s3.us-west-2.amazonaws.com/poke-cli-card-v1.8.8.gif) --- ## Installation From c3739f1b358be731ba4315c0f4e4481b06c61e3f Mon Sep 17 00:00:00 2001 From: Christian Sanchez Date: Fri, 6 Feb 2026 15:38:26 -0800 Subject: [PATCH 11/13] fixing pyrefly type checking error --- card_data/pipelines/defs/extract/tcgcsv/extract_pricing.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/card_data/pipelines/defs/extract/tcgcsv/extract_pricing.py b/card_data/pipelines/defs/extract/tcgcsv/extract_pricing.py index c80df0e..3edf3dc 100644 --- a/card_data/pipelines/defs/extract/tcgcsv/extract_pricing.py +++ b/card_data/pipelines/defs/extract/tcgcsv/extract_pricing.py @@ -147,10 +147,14 @@ def pull_product_information(set_number: str) -> pl.DataFrame: if any(variant in card_name for variant in skip_variants): continue + card_number = get_card_number(card) + if card_number is None: + continue + card_info = { "product_id": card["productId"], "name": extract_card_name(card_name), - "card_number": get_card_number(card), + "card_number": card_number, "market_price": price_dict.get(card["productId"]), } cards_data.append(card_info) From e64fec7cd95d1c8cd41954753c03d468e55e0244 Mon Sep 17 00:00:00 2001 From: Christian Sanchez Date: Fri, 6 Feb 2026 15:45:16 -0800 Subject: [PATCH 12/13] minor edits --- .github/workflows/go_lint.yml | 2 +- README.md | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/go_lint.yml b/.github/workflows/go_lint.yml index 3b97a1f..6c06f7f 100644 --- a/.github/workflows/go_lint.yml +++ b/.github/workflows/go_lint.yml @@ -1,4 +1,4 @@ -name: Lint +name: Golang Lint on: pull_request: diff --git a/README.md b/README.md index e68681a..bdf0a4e 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,7 @@ View the [documentation](https://docs.poke-cli.com) on the data infrastructure i * [Tested Terminals](#tested-terminals) --- + ## Demo ### Video Game Data @@ -31,7 +32,9 @@ View the [documentation](https://docs.poke-cli.com) on the data infrastructure i ### Trading Card Game Data ![demo-tcg](https://poke-cli-s3-bucket.s3.us-west-2.amazonaws.com/poke-cli-card-v1.8.8.gif) + --- + ## Installation * [Homebrew](#homebrew) From 2612e636a7313ed73c506baf8eef1ce17a992b1d Mon Sep 17 00:00:00 2001 From: Christian Sanchez Date: Fri, 6 Feb 2026 15:45:24 -0800 Subject: [PATCH 13/13] initial commit --- .github/workflows/python_typing.yml | 35 +++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 .github/workflows/python_typing.yml diff --git a/.github/workflows/python_typing.yml b/.github/workflows/python_typing.yml new file mode 100644 index 0000000..c84923e --- /dev/null +++ b/.github/workflows/python_typing.yml @@ -0,0 +1,35 @@ +name: Python Type Check + +on: + pull_request: + types: [opened, reopened, synchronize] + paths: + - 'card_data/**' + +permissions: + contents: read + +jobs: + pyrefly: + runs-on: ubuntu-22.04 + defaults: + run: + working-directory: card_data + + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Setup Python + uses: actions/setup-python@v6 + with: + python-version: '3.12' + + - name: Install uv + uses: astral-sh/setup-uv@v7 + + - name: Install dependencies + run: uv sync --dev + + - name: Type check + run: uvx pyrefly check --summarize-errors \ No newline at end of file