FlowBuilder#
FlowBuilder is the base class for every per-platform donation flow. It
owns the complete lifecycle of a single platform’s donation: prompting for
a file, validating it, extracting data, presenting a consent form, and
donating the result.
File: packages/python/port/helpers/flow_builder.py
How it fits in#
script.py is the study-level orchestrator. It iterates through a registry
of platforms and for each one creates a FlowBuilder subclass instance and
calls yield from flow.start_flow(). The per-platform flow runs completely
before the next platform begins.
flowchart LR
subgraph script.py
P["process(session_id, platform)"]
end
subgraph platforms
LI["LinkedInFlow"]
IG["InstagramFlow"]
FB["FacebookFlow"]
OT["... (10 platforms)"]
end
subgraph FlowBuilder
SF["start_flow()"]
end
P -- "yield from" --> LI & IG & FB & OT
LI & IG & FB & OT -- "inherits" --> SF
The 11-step flow#
start_flow() is a generator that implements a fixed lifecycle. It loops
so the participant can retry file selection, and breaks out of the loop once
extraction succeeds.
flowchart TD
A["1. Render file prompt\nCommandUIRender + PropsUIPromptFileInput"]
B{"PayloadFile\nor PayloadString?"}
C["2. Materialize file\nuploads.materialize_file()"]
D["3. Safety check\nuploads.check_file_safety()"]
E{"Safe?"}
F["Render safety error page\nreturn"]
G["4. Validate\nself.validate_file(path)"]
H{"Valid?\nstatus == 0"}
I["5. Retry prompt\nCommandUIRender + PropsUIPromptConfirm"]
J{"Try again?"}
K["6. Extract\nself.extract_data(path, validation)"]
L["7. Log extraction summary\nph.emit_log() — counts only"]
M{"Any tables?"}
N["Render no-data page\nreturn"]
O["8. Render consent form\nCommandUIRender + PropsUIPromptConsentFormViz"]
P{"PayloadJSON\nor PayloadFalse?"}
Q["9. Donate\nCommandSystemDonate(session_id-platform, json)"]
R{"Donation\nsucceeded?"}
S["Render failure page\nreturn"]
T["Flow complete\nreturn"]
A --> B
B -- "no payload" --> T
B -- "yes" --> C --> D --> E
E -- "no" --> F
E -- "yes" --> G --> H
H -- "no" --> I --> J
J -- "PayloadTrue" --> A
J -- "PayloadFalse" --> T
H -- "yes" --> K --> L --> M
M -- "no tables" --> N
M -- "yes" --> O --> P
P --> Q --> R
R -- "failed + not decline" --> S
R -- "success or decline" --> T
Emit log milestones#
start_flow() calls ph.emit_log() at each significant step. These are the
messages that appear in the host’s log stream. They are always PII-free —
platform name, status code, and counts, never participant data.
Step |
Log message |
|---|---|
File received |
|
Safety check failed |
|
Validation passed |
|
Validation failed |
|
Extraction complete |
|
Consent form shown |
|
Consent accepted |
|
Consent declined |
|
Donation started |
|
Donation result |
|
Implementing a platform#
Subclass FlowBuilder and implement two methods:
class LinkedInFlow(FlowBuilder):
def __init__(self, session_id: str):
super().__init__(session_id, "LinkedIn") # sets self.platform_name
def validate_file(self, file: str) -> validate.ValidateInput:
return validate.validate_zip(DDP_CATEGORIES, file)
def extract_data(self, file: str, validation: validate.ValidateInput) -> ExtractionResult:
return extraction(file, validation)
validate_file(path)— returns aValidateInput. Status 0 = valid; non-zero = invalid.extract_data(path, validation)— returns anExtractionResult. Can also be a generator (yield from) if you need to yield intermediate commands during extraction.
Everything else — the file prompt, the retry loop, the consent form, the
donation, the logging — is handled by start_flow().
UI text#
FlowBuilder.__init__() calls _initialize_ui_text(), which builds a dict
of Translatable strings for the file prompt header, consent form header,
retry header, and review description. These are constructed from
self.platform_name so they automatically use the platform name you pass to
super().__init__().
Override _initialize_ui_text() or modify self.UI_TEXT after super().__init__()
if you need custom text.
Key files#
File |
Role |
|---|---|
|
|
|
|
|
Example platform implementation |
|
|
→ Extraction — how extract_data() works inside a platform