diff --git a/.github/workflows/maintenance-check.yml b/.github/workflows/maintenance-check.yml index 45a3b24..e882515 100644 --- a/.github/workflows/maintenance-check.yml +++ b/.github/workflows/maintenance-check.yml @@ -71,49 +71,229 @@ jobs: name: audit-report path: specs/audit-report.md - - name: Build issue body + - name: Classify severity and build issue + id: classify if: steps.fetch.outputs.changes == 'true' run: | + SEVERITY="low" + DATE=$(date -u +%Y-%m-%d) + + # --- Parse diff report for high/medium signals --- + DIFF_FILE="specs/diff-report.md" + NEW_ENDPOINTS=0 + REMOVED_ENDPOINTS=0 + CHANGED_ENDPOINTS=0 + SCHEMA_CHANGES=0 + + if [ -f "$DIFF_FILE" ]; then + # Count sections that have content (not just "No ... endpoints/changes") + if ! grep -q "No new endpoints" "$DIFF_FILE"; then + NEW_ENDPOINTS=$(grep -c '^\- \*\*' "$DIFF_FILE" 2>/dev/null || echo 0) + fi + if ! grep -q "No removed endpoints" "$DIFF_FILE"; then + REMOVED_ENDPOINTS=1 + fi + if ! grep -q "No changed endpoints" "$DIFF_FILE"; then + CHANGED_ENDPOINTS=1 + fi + if ! grep -q "No schema changes" "$DIFF_FILE"; then + SCHEMA_CHANGES=1 + fi + fi + + # --- Parse audit report for drift signals --- + AUDIT_FILE="specs/audit-report.md" + MISSING_ENDPOINTS=0 + BODY_DRIFT=0 + PARAM_DRIFT=0 + CODE_ISSUES=0 + + if [ -f "$AUDIT_FILE" ]; then + MISSING_ENDPOINTS=$(grep -oP 'Missing from SDK: \K[0-9]+' "$AUDIT_FILE" | head -1 || echo 0) + [ -z "$MISSING_ENDPOINTS" ] && MISSING_ENDPOINTS=0 + + if ! grep -q "No request body drift" "$AUDIT_FILE"; then + BODY_DRIFT=1 + fi + if ! grep -q "No query/path parameter drift" "$AUDIT_FILE"; then + PARAM_DRIFT=1 + fi + CODE_ISSUES=$(grep -oP 'Code issues found: \K[0-9]+' "$AUDIT_FILE" | head -1 || echo 0) + [ -z "$CODE_ISSUES" ] && CODE_ISSUES=0 + fi + + # --- Determine severity --- + # High: removed endpoints, missing endpoints, breaking schema changes, body drift + if [ "$REMOVED_ENDPOINTS" -gt 0 ] || [ "$MISSING_ENDPOINTS" -gt 0 ] || [ "$BODY_DRIFT" -gt 0 ]; then + SEVERITY="high" + # Medium: new endpoints, changed endpoints, param drift, schema changes, real code issues + elif [ "$NEW_ENDPOINTS" -gt 0 ] || [ "$CHANGED_ENDPOINTS" -gt 0 ] || [ "$PARAM_DRIFT" -gt 0 ] || [ "$SCHEMA_CHANGES" -gt 0 ]; then + SEVERITY="medium" + # Low: cosmetic changes only (descriptions, formatting) + else + SEVERITY="low" + fi + + echo "severity=$SEVERITY" >> $GITHUB_OUTPUT + echo "date=$DATE" >> $GITHUB_OUTPUT + + # --- Build summary line --- + SUMMARY="" + [ "$REMOVED_ENDPOINTS" -gt 0 ] && SUMMARY="${SUMMARY}removed endpoints, " + [ "$MISSING_ENDPOINTS" -gt 0 ] && SUMMARY="${SUMMARY}${MISSING_ENDPOINTS} missing SDK methods, " + [ "$BODY_DRIFT" -gt 0 ] && SUMMARY="${SUMMARY}request body drift, " + [ "$PARAM_DRIFT" -gt 0 ] && SUMMARY="${SUMMARY}parameter drift, " + [ "$NEW_ENDPOINTS" -gt 0 ] && SUMMARY="${SUMMARY}${NEW_ENDPOINTS} new endpoints, " + [ "$CHANGED_ENDPOINTS" -gt 0 ] && SUMMARY="${SUMMARY}changed endpoints, " + [ "$SCHEMA_CHANGES" -gt 0 ] && SUMMARY="${SUMMARY}schema changes, " + [ "$CODE_ISSUES" -gt 0 ] && SUMMARY="${SUMMARY}${CODE_ISSUES} code issues, " + + if [ -z "$SUMMARY" ]; then + SUMMARY="cosmetic spec changes only" + else + SUMMARY="${SUMMARY%, }" + fi + + # --- Build issue body --- { - echo "## Etsy API Drift Detected" - echo "" - echo "The weekly maintenance check found changes in the Etsy OpenAPI spec." + echo "## Etsy API Drift — \`${SEVERITY}\` severity" echo "" - echo "### Diff Report" + echo "**Detected:** ${DATE} | **Severity:** \`${SEVERITY}\` | **Summary:** ${SUMMARY}" echo "" - cat specs/diff-report.md + + echo "### What Changed (Diff Report)" echo "" - echo "### SDK Coverage" + if [ -f "$DIFF_FILE" ]; then + cat "$DIFF_FILE" + else + echo "No diff report generated." + fi echo "" - sed -n '/## Coverage Summary/,/^## [^C]/p' specs/audit-report.md | head -20 + + echo "### SDK Impact (Audit Report)" echo "" + if [ -f "$AUDIT_FILE" ]; then + # Coverage summary + sed -n '/## Coverage Summary/,/^## Missing Endpoints/p' "$AUDIT_FILE" | head -15 + echo "" + + # Missing endpoints (if any) + MISSING_SECTION=$(sed -n '/## Missing Endpoints/,/^## Not Implemented/p' "$AUDIT_FILE") + if ! echo "$MISSING_SECTION" | grep -q "All OAS operations are covered"; then + echo "#### Missing Endpoints" + echo "$MISSING_SECTION" | tail -n +2 | head -20 + echo "" + fi + + # Body drift (if any) + if ! grep -q "No request body drift" "$AUDIT_FILE"; then + echo "#### Request Body Drift" + sed -n '/## Request Body Drift/,/^## /p' "$AUDIT_FILE" | head -30 + echo "" + fi + + # Param drift (if any) + if ! grep -q "No query/path parameter drift" "$AUDIT_FILE"; then + echo "#### Parameter Drift" + sed -n '/## Query\/Path Parameter Drift/,/^## /p' "$AUDIT_FILE" | head -30 + echo "" + fi + + # Enum staleness (if any — only show real drift, not the known holiday IDs) + ENUM_SECTION=$(sed -n '/## Enum Staleness/,/^## /p' "$AUDIT_FILE") + if [ -n "$ENUM_SECTION" ] && ! echo "$ENUM_SECTION" | grep -q "^$"; then + ENUM_LINE_COUNT=$(echo "$ENUM_SECTION" | wc -l) + if [ "$ENUM_LINE_COUNT" -gt 3 ]; then + echo "#### Enum Staleness" + echo "$ENUM_SECTION" | head -30 + echo "" + fi + fi + + # Code issues (if any) + if [ "$CODE_ISSUES" -gt 0 ]; then + echo "#### Code Issues (${CODE_ISSUES})" + sed -n '/## Code Issues/,/^## /p' "$AUDIT_FILE" | head -30 + echo "" + fi + + # Extra SDK methods + EXTRA_SECTION=$(sed -n '/## Extra SDK Methods/,/^## /p' "$AUDIT_FILE") + if ! echo "$EXTRA_SECTION" | grep -q "no matching OAS operation" || echo "$EXTRA_SECTION" | grep -q "\*\*"; then + echo "
Extra SDK Methods (no OAS match)" + echo "" + echo "$EXTRA_SECTION" | head -20 + echo "" + echo "
" + echo "" + fi + fi + echo "---" echo "" - echo "**Next steps:**" - echo "1. Review the changes above" - echo '2. Run `/maintain-audit` to audit SDK coverage and implement changes' - echo "3. Update baseline: \`cp specs/latest.json specs/baseline.json\`" + echo "### Action Required" + echo "" + if [ "$SEVERITY" = "high" ]; then + echo "**Immediate action needed.** Breaking changes or missing SDK coverage detected." + echo "" + echo "1. Run \`/maintain-audit\` to review and implement fixes" + echo "2. Prioritize request body drift and missing endpoints" + elif [ "$SEVERITY" = "medium" ]; then + echo "**Review recommended.** New or changed endpoints that may need SDK updates." + echo "" + echo "1. Run \`/maintain-audit\` to review changes" + echo "2. Check if new endpoints need SDK methods" + else + echo "**Low priority.** Cosmetic spec changes only — no SDK code impact." + echo "" + echo "1. Review to confirm no hidden semantic changes" + echo "2. Update baseline if confirmed: \`cp specs/latest.json specs/baseline.json\`" + fi echo "" echo "*Auto-generated by [Maintenance Check](${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID})*" } > issue-body.md + - name: Ensure severity labels exist + if: steps.fetch.outputs.changes == 'true' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh label create "severity: high" --color "B60205" --description "High severity — breaking changes or missing coverage" --force 2>/dev/null || true + gh label create "severity: medium" --color "D93F0B" --description "Medium severity — new/changed endpoints need review" --force 2>/dev/null || true + gh label create "severity: low" --color "0E8A16" --description "Low severity — cosmetic or informational only" --force 2>/dev/null || true + - name: Create or update issue on API drift if: steps.fetch.outputs.changes == 'true' env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + SEVERITY: ${{ steps.classify.outputs.severity }} + DATE: ${{ steps.classify.outputs.date }} run: | + TITLE="audit: Spec Drift [${DATE}] — ${SEVERITY}" EXISTING=$(gh issue list --label "api-drift" --state open --json number --jq '.[0].number // empty') if [ -n "$EXISTING" ]; then echo "Updating existing issue #${EXISTING}" - gh issue edit "$EXISTING" --body-file issue-body.md - gh issue comment "$EXISTING" --body "Updated $(date -u +%Y-%m-%dT%H:%M:%SZ): New spec changes detected. Issue body updated with latest diff." + gh issue edit "$EXISTING" \ + --title "$TITLE" \ + --body-file issue-body.md \ + --add-label "severity: ${SEVERITY}" \ + --add-assignee amitray007 + # Remove stale severity labels + for LBL in high medium low; do + if [ "$LBL" != "$SEVERITY" ]; then + gh issue edit "$EXISTING" --remove-label "severity: $LBL" 2>/dev/null || true + fi + done + gh issue comment "$EXISTING" --body "Updated ${DATE}: Spec changes re-evaluated. Severity: \`${SEVERITY}\`." else echo "Creating new api-drift issue" gh issue create \ - --title "Etsy API drift detected" \ + --title "$TITLE" \ --body-file issue-body.md \ - --label "api-drift" + --label "api-drift" \ + --label "severity: ${SEVERITY}" \ + --assignee amitray007 fi - name: Close drift issue if no changes diff --git a/etsy_python/v3/models/Listing.py b/etsy_python/v3/models/Listing.py index 1e69f61..b2a5363 100644 --- a/etsy_python/v3/models/Listing.py +++ b/etsy_python/v3/models/Listing.py @@ -71,7 +71,7 @@ def __init__( personalization_is_required: Optional[bool] = None, personalization_char_count_max: Optional[int] = None, personalization_instructions: Optional[str] = None, - production_partner_ids: Optional[int] = None, + production_partner_ids: Optional[List[int]] = None, image_ids: Optional[List[int]] = None, is_supply: Optional[bool] = None, is_customizable: Optional[bool] = None, @@ -153,7 +153,7 @@ class UpdateListingRequest(Request): def __init__( self, - image_ids: Optional[List[str]] = None, + image_ids: Optional[List[int]] = None, title: Optional[str] = None, description: Optional[str] = None, materials: Optional[List[str]] = None, @@ -323,7 +323,7 @@ def __init__( class UpdateVariationImagesRequest(Request): nullable: List[str] = [] - mandatory: List[str] = [] + mandatory: List[str] = ["variation_images"] def __init__(self, variation_images: List[Dict[str, Any]]) -> None: self.variation_images = variation_images diff --git a/etsy_python/v3/resources/HolidayPreferences.py b/etsy_python/v3/resources/HolidayPreferences.py index e626455..47b24a8 100644 --- a/etsy_python/v3/resources/HolidayPreferences.py +++ b/etsy_python/v3/resources/HolidayPreferences.py @@ -1,4 +1,5 @@ from dataclasses import dataclass +from enum import Enum from typing import Optional, Union from etsy_python.v3.enums.HolidayPreferences import CA_HOLIDAYS, HOLIDAYS, US_HOLIDAYS @@ -20,10 +21,11 @@ def get_holiday_preferences(self, shop_id: int) -> Union[Response, RequestExcept def update_holiday_preferences( self, shop_id: int, - holiday_id: Optional[Union[HOLIDAYS, US_HOLIDAYS, CA_HOLIDAYS, int]], + holiday_id: Union[HOLIDAYS, US_HOLIDAYS, CA_HOLIDAYS, int], holiday_preference: UpdateHolidayPreferencesRequest, ) -> Union[Response, RequestException]: - endpoint = f"/shops/{shop_id}/holiday-preferences/{holiday_id}" + hid = holiday_id.value if isinstance(holiday_id, Enum) else holiday_id + endpoint = f"/shops/{shop_id}/holiday-preferences/{hid}" return self.session.make_request( endpoint, method=Method.PUT, payload=holiday_preference ) diff --git a/specs/baseline.json b/specs/baseline.json index c5a0303..8e50488 100644 --- a/specs/baseline.json +++ b/specs/baseline.json @@ -2388,7 +2388,7 @@ }, "put": { "operationId": "updateListingInventory", - "description": "
General ReleaseReport bug

This endpoint is ready for production use.

\n\nUpdates the inventory for a listing identified by a listing ID. The update fails if the supplied values for product sku, offering quantity, and/or price are incompatible with values in `*_on_property_*` fields. When setting a price, assign a float equal to amount divided by divisor as specified in the Money resource.", + "description": "
General ReleaseReport bug

This endpoint is ready for production use.

\n\nUpdates the inventory for a listing identified by a listing ID. The update fails if the supplied values for product sku, offering quantity, and/or price are incompatible with values in `*_on_property` fields. When setting a price, assign a float equal to amount divided by divisor as specified in the Money resource.", "tags": [ "ShopListing Inventory" ], diff --git a/tests/conftest.py b/tests/conftest.py index 4f50aad..539552a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -29,7 +29,7 @@ MOCK_PRODUCT_ID = 70707 MOCK_OFFERING_ID = 80808 MOCK_USER_ADDRESS_ID = 90909 -MOCK_HOLIDAY_ID = "thanksgiving" +MOCK_HOLIDAY_ID = 10 @pytest.fixture diff --git a/tests/fixtures/responses.py b/tests/fixtures/responses.py index 5a01ddf..d7d4f04 100644 --- a/tests/fixtures/responses.py +++ b/tests/fixtures/responses.py @@ -589,8 +589,8 @@ def make_user_address(**overrides): def make_holiday_preference(**overrides): data = { - "holiday_id": "thanksgiving", - "holiday_name": "Thanksgiving", + "holiday_id": 10, + "holiday_name": "Thanksgiving Day", "is_working": False, } data.update(overrides) diff --git a/tests/test_listing_models.py b/tests/test_listing_models.py index d6be824..7709b9f 100644 --- a/tests/test_listing_models.py +++ b/tests/test_listing_models.py @@ -141,6 +141,10 @@ def test_valid_request(self): result = req.get_dict() assert len(result["variation_images"]) == 1 + def test_missing_variation_images_raises(self): + with pytest.raises(ValueError): + UpdateVariationImagesRequest(variation_images=None) + class TestUploadListingImageRequest: def test_sets_file_and_data(self): diff --git a/tests/test_remaining_resources.py b/tests/test_remaining_resources.py index 7450a74..c50177e 100644 --- a/tests/test_remaining_resources.py +++ b/tests/test_remaining_resources.py @@ -18,6 +18,7 @@ CreateShopReturnPolicyRequest, UpdateShopReturnPolicyRequest, ) +from etsy_python.v3.enums.HolidayPreferences import US_HOLIDAYS from etsy_python.v3.models.HolidayPreferences import UpdateHolidayPreferencesRequest from etsy_python.v3.resources.HolidayPreferences import HolidayPreferencesResource from etsy_python.v3.resources.ListingFile import ListingFileResource @@ -645,6 +646,21 @@ def test_update_holiday_preferences(self, mock_session): payload=payload, ) + def test_update_holiday_preferences_with_enum(self, mock_session): + mock_session.make_request.return_value = Response( + 200, make_holiday_preference() + ) + resource = HolidayPreferencesResource(session=mock_session) + payload = MagicMock(spec=UpdateHolidayPreferencesRequest) + resource.update_holiday_preferences( + MOCK_SHOP_ID, US_HOLIDAYS.THANKSGIVING_DAY, payload + ) + mock_session.make_request.assert_called_once_with( + f"/shops/{MOCK_SHOP_ID}/holiday-preferences/{US_HOLIDAYS.THANKSGIVING_DAY.value}", + method=Method.PUT, + payload=payload, + ) + # --- ListingOffering --- class TestListingOfferingResource: