Skip to content

Feature/funpayparsers 080 sync#35

Open
Flummy1 wants to merge 14 commits intodevfrom
feature/funpayparsers-080-sync-v2
Open

Feature/funpayparsers 080 sync#35
Flummy1 wants to merge 14 commits intodevfrom
feature/funpayparsers-080-sync-v2

Conversation

@Flummy1
Copy link
Copy Markdown
Contributor

@Flummy1 Flummy1 commented Apr 20, 2026

Summary

  • Bumps funpayparsers dependency from >=0.7.0,<0.8.0 to >=0.8.0,<0.9.0
  • Adds new types introduced in funpayparsers 0.8.0 to funpaybotengine.types

Changes

New module — types/subcategory_structure.py

Re-exports three new types from funpayparsers.types.subcategory_structure:

  • FieldCondition — visibility condition for a subcategory field (field_id + set of trigger values)
  • SubcategoryFieldDef — full definition of a single subcategory field (id, type, label, conditions, options)
  • SubcategoryStructure — derived structure for O(1) lookup by field ID, with label_map and lower_label_map (case-insensitive)

Note: SubcategoryFieldType enum is already available via enums.py (re-exported from funpayparsers).

OfferFields — two new members

  • field_schema: list[SubcategoryFieldDef] — populated by OfferFieldsParser from the data-fields JSON attribute; empty list for chips offers
  • subcategory_structure property — builds a SubcategoryStructure from field_schema on demand

Breaking changes

None. All additions are backwards-compatible (field_schema defaults to [])

Flummy1 and others added 3 commits April 20, 2026 13:50
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ypes

Re-export FieldCondition, SubcategoryFieldDef, SubcategoryStructure from
funpayparsers.types.subcategory_structure via a new types module.

Add field_schema: list[SubcategoryFieldDef] field to OfferFields and a
subcategory_structure property that builds a SubcategoryStructure on demand.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
FieldCondition, SubcategoryFieldDef and SubcategoryStructure are now
proper Pydantic models inheriting from FunPayObject instead of
re-exporting the funpayparsers dataclasses directly.

Each class owns its _add_raw_source validator:
- FieldCondition/SubcategoryStructure: no raw_source on the parser side,
  generate a stable JSON identifier on construction
- SubcategoryFieldDef: raw_source already present on the parser dataclass,
  let from_attributes carry it through; fallback for dict construction

SubcategoryStructure.from_offer_fields is added as a classmethod and used
by OfferFields.subcategory_structure property. OfferFields.field_schema now
stores engine SubcategoryFieldDef instances.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@Flummy1 Flummy1 requested a review from qvvonk as a code owner April 20, 2026 10:51
Flummy1 added 11 commits May 2, 2026 16:05
…rets'

Mirrors funpayparsers cbbbd5b. Without this, saving auto-delivery secrets
through the engine writes the wrong form key and FunPay silently drops it.
Mirrors funpayparsers PR #102 (b942fae). The listing page parser
populates SubcategoryStructure from data-fields JSON; without this
field the structure was silently dropped on the engine side.
…e matching with parser

- label_map / lower_label_map now return dict[str, list[str]] and use
  cached_property, matching funpayparsers 66e6cd0. Previously a single-id
  dict silently dropped fields with duplicate labels (notably empty
  labels on form fields without <label>).
- FieldCondition.is_satisfied_by and stored values are case-insensitive
  via casefold, mirroring parser fc6190d. FunPay's listing page and
  offerEdit form render the same value with inconsistent casing.
Mirrors funpayparsers f2d2851. The parser stamps subcategory context
(OFFERS/CHIPS) onto every preview based on the offer URL; the engine
wrapper was dropping it.
…tegoryStructure validator

The parser ships SubcategoryStructure as a plain @DataClass (no
FunPayObject mixin). When pydantic ingests a parser instance via
model_validate, the existing 'before' validator only handled dict
input and returned dataclass instances unchanged, causing
'raw_source: Field required' validation errors on every page that
actually has a populated structure (e.g. subcat 1, 27, 41).

Mirrors the same pattern already used for FieldCondition. Verified on
live subcategories with rich filter blocks.
…fferPage.images

Mirrors funpayparsers (types/pages/order_page.py:53, types/pages/offer_page.py:48).
Both helpers were missing in engine, breaking the documented flow:

  message.meta.order_id -> get_order_page -> structure -> field_id mapping.

Also adds OfferPage.images (engine wrapper was missing the list parsed
from div.param-list attachments).

Verified end-to-end on live FunPay session.
…ields, OrderPage data split

Surfaces the new structural-parsing primitives from funpayparsers
feature/multi-source-label-index in the engine wrappers:

- SubcategoryFieldDef.aliases (casefolded set of cross-locale label aliases)
- SubcategoryStructure.label_map / lower_label_map now index aliases too,
  plus lookup_field_id(), add_alias(), enrich_from_offer() helpers
- OfferPreview.parse_title_fields(structure) and
  OrderPreview.parse_title_fields(structure) — delegate to parser's
  _parse_title_fields, extract NUMERIC_RANGE/SELECT/DROPDOWN suffix fields
  from comma-separated titles
- OrderPage.metadata (canonical-keyed stable order metadata) and
  lot_fields (everything else); get_structured_fields now operates on
  lot_fields, eliminating false matches against order metadata labels.
  Properties (short_description, amount, …) read from metadata directly.
- OrderPage.data kept for backwards compatibility.

Live recall on real subcategories:
- OfferPage.get_structured_fields: 0 → 3 matches after enrich_from_offer
- OrderPreview.parse_title_fields on subcat #1316 yields
  {currency: USD, usd: 20 USD} from a real purchase title.
…el seed, derived_from

Surfaces the new parser-side primitives from funpayparsers PR #104
(commit 8d979c5, version 0.9.0) in the engine:

types/subcategory_structure.py
- SubcategoryFieldDef: model_validator(after) seeds aliases with the
  canonical localized label automatically (mirrors parser
  __post_init__). Single-source structures now resolve via
  lookup_field_id(label) without an explicit add_alias call.
- SubcategoryStructure: add derived_from: Literal['lot_fields',
  'chips_offers'] and is_synthetic property so callers can distinguish
  authoritative vs. inferred structures.
- SubcategoryStructure.enrich_from_offer_fields(OfferFields): bridge
  the listing-form / offerEdit-form locale gap by registering each
  offerEdit <label> as an alias on the matching field id.

methods/get_subcategory_page.py
- GetSubcategoryPage now accepts options: SubcategoryPageParsingOptions
  | None and forwards it to base parser_options. Required to enable
  fallback_structure_from_chips_offers.

client/bot.py
- Bot.get_subcategory_page(...) accepts options.
- Bot.get_subcategory_structure(stype, sid, *, synthesize_chips=True,
  seed_from_offer_fields=True, enrich_from_offer_sample=True): new
  high-level helper that runs the full structure pipeline in one call.
  Uses get_offer_fields(subcategory=..) so it works on any subcategory
  regardless of ownership.

Live verification on golden_key audit (62 purchases):
- CHIPS #173: structure=None → structure(1 field, is_synthetic=True).
- Steam #1086: 'способ пополнения' now resolves to `type` field via
  offerEdit-seeded alias.
- Aggregate lot recall 74% → 75%; 'no_structure' orders 2 → 1.
…ers + AliasSource provenance

Mirrors the new API from funpayparsers PR #104 follow-up commit 54e4d56:

types/subcategory_structure.py
- Re-export AliasSource enum (LABEL/LISTING/OFFER_EDIT/OFFER_PAGE/
  ORDER_PAGE/OFFER_PREVIEW/USER) so engine consumers don't need to
  import from funpayparsers directly.
- _alias_sources: PrivateAttr[dict[(field_id, alias), AliasSource]]
  tracks provenance for every alias added via add_alias.
- add_alias gains optional source: AliasSource = USER parameter.
- alias_source / forget_aliases_from helpers for inspection /
  invalidation by provenance.
- enrich_from_order_page(OrderPage): value-match on order.lot_fields.
- enrich_from_offer_previews(Iterable[OfferPreview]): zero-HTTP batch
  enrichment from other_data_names of listing/profile/my-offers
  previews.
- Existing enrichers pass appropriate source values for traceability.

client/bot.py
- Bot.get_subcategory_structure pipeline inserts the free batch
  enrich_from_offer_previews(page.offers) step. No new arg — default-on
  since it has no extra HTTP cost.

Live verification on 62-purchase audit: all new methods callable on
engine wrapper; provenance correctly recorded. Aggregate lot recall
stable at 75% — new methods are structurally correct but remaining
gaps are FunPay-side locale inconsistencies (e.g. order_page 'Global'
vs offerEdit options ['Европа', 'Америка', ...]) that no client-side
heuristic can bridge.
…ext lookup, structure merge

Mirrors funpayparsers PR #104 commit e78cc88 (v0.9.2):

types/subcategory_structure.py
- SubcategoryStructure.delivery_fields: dict[str, str] — per-subcat
  ``input.name → label`` map, accumulated via
  enrich_delivery_fields_from_offer.
- enrich_delivery_fields_from_offer(OfferPage): unions
  OfferPage.delivery_fields_spec into delivery_fields.
- lookup_field_id gains optional context kwarg. When the label resolves
  to multiple field ids, prefers the field whose visibility conditions
  are satisfied by already-resolved context values. Disambiguates
  shared-label fields like quantity/quantity2.
- merge_from(other): unions fields/aliases/delivery_fields from another
  structure; deep-copies new fields, preserves alias provenance. Used
  to combine synthetic listing-derived and from_offer_fields-derived
  structures, or hydrate persistent-cached aliases.

types/pages/offer_page.py
- OfferPage.delivery_fields_spec: dict[str, str] — parsed from the
  buyer's order form on /lots/offer?id=... .

types/pages/order_page.py
- OrderPage.delivery_fields: per-order delivery data classified via
  parser's static blacklist at parse time.
- OrderPage.get_structured_fields uses lookup_field_id with
  per-iteration context for shared-label disambiguation.
- OrderPage.reclassify_with_structure(structure): re-splits data using
  structure.delivery_fields for higher precision.

client/bot.py
- Bot.get_subcategory_structure: the offer-page sample step now also
  calls enrich_delivery_fields_from_offer(op) on the same OfferPage,
  accumulating per-subcat delivery labels at zero additional HTTP.

API-surface verified end-to-end. Live recall re-measurement pending
FunPay availability (currently mass-timing-out).
…elivery

types/pages/order_page.py
- OrderPage.opened_at / closed_at: datetime | None — UTC datetimes parsed
  from metadata['open'] / metadata['closed'] via
  funpayparsers.parsers.utils.parse_date_string. Existing
  open_date_text / close_date_text retained for callers that need the
  raw FunPay rendering.
- OrderPage.recipient / recipient_label — best-effort buyer-supplied
  delivery target: the first non-empty value from delivery_fields and
  its label (Telegram username, Steam login, character name, email).
- OrderPage.get_structured_delivery(structure=None) — returns
  delivery_fields keyed by canonical FunPay form input names when
  structure.delivery_fields is populated (label → input-name reverse
  map); falls back to raw labels otherwise.
- OrderPage.get_structured_context(structure=None) — unified rich view
  combining lot, delivery, normalized amount/dates/total/category
  in a single dict. amount falls back from metadata['amount'] to the
  leading integer of lot['quantity'] / lot['quantity2'] so quantity
  is exposed regardless of which FunPay surface carried it (e.g.
  Telegram Stars subcategory routes quantity through lot, not
  metadata).

Live verification (4 representative orders):
- Telegram Stars: amount=50 (from lot.quantity='50 звёзд'),
  recipient='...' (telegram username), opened_at/closed_at as UTC.
- Steam Пополнение: amount=1500 (metadata), recipient=steam login.
- App Store gift card: amount=2 (metadata), no delivery.
- Roblox: amount=100 (from lot.quantity='100 робуксов').

Invariant `amount OR recipient is not None` holds across all cases
where the order carries either piece of information.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant