Skip to content

Easy EU region selection: ROBOFLOW_REGION, auth login --region, auth set-region - #513

Open
imbgar-roboflow wants to merge 1 commit into
mainfrom
brandongarcia/inf-314-cli-eu-region
Open

Easy EU region selection: ROBOFLOW_REGION, auth login --region, auth set-region#513
imbgar-roboflow wants to merge 1 commit into
mainfrom
brandongarcia/inf-314-cli-eu-region

Conversation

@imbgar-roboflow

Copy link
Copy Markdown
Contributor

Description

One switch points the SDK + CLI at the Roboflow EU data-residency platform (Linear INF-314). Three ways to set it:

roboflow auth login --region eu    # browser auth flow opens/validates against https://app.roboflow.eu/auth-cli
roboflow auth set-region eu        # persistent config setting, shown in 'roboflow auth status'
ROBOFLOW_REGION=eu ...             # env var, for CI/scripts

Sibling PR for inference-cli: roboflow/inference#2701.

How it works

  • config.py gains a region registry (us default / eu) and runtime resolvers (get_effective_region(), resolve_url()). Per-URL precedence: explicit env var > explicit config-file key > region default > existing US default — so API_URL-style overrides (the staging escape hatch) keep working unchanged.
  • EU map (all verified live): api/appapi/app.roboflow.eu, detection + instance-seg → serverless.roboflow.eu, dedicated deployments → eu.roboflow.cloud. Universe and semantic-seg stay on global .com — no EU instance exists.
  • login() resolves the app host at call time (an import-time constant can't see a --region flag passed in the same process) and now merges the prior config on forced re-login instead of clobbering it, so region + URL overrides survive.
  • auth login --api-key --region eu validates the key against the EU API; auth status (text + --json) reports region and effective URLs; the hidden root roboflow login alias forwards --region.
  • Unknown region values warn on stderr and fall back to us (never raise at import).

With no region set anywhere, every URL constant is byte-identical to today — guarded by a test.

Testing

  • 937 tests green (python -m unittest), including 16 new ones: back-compat guard, precedence matrix, EU mapping, login persistence/merge (incl. --force), CLI paths, invalid-region handling
  • make check_code_quality (ruff + mypy) clean
  • Live smoke against real EU endpoints: interactive login prints/validates against app.roboflow.eu, --api-key path validates against api.roboflow.eu, error behavior identical to US

Docs

  • CLI-COMMANDS.md: region flag, set-region, ROBOFLOW_REGION, endpoint table
  • README.md: "Using Roboflow EU" section

@imbgar-roboflow

Copy link
Copy Markdown
Contributor Author

CI note: the check_code_quality failures here are unrelated to this diff — ruff 0.16.0 (released today) stabilized the CPY copyright rules, which select = ["ALL"] + unpinned CI ruff now enforce repo-wide. Main is red with the same 72 CPY001 errors. One-line fix in #514; will rebase/rerun once that merges.

…egion, auth set-region

One switch selects the Roboflow platform region (us default, eu):
resolved env var > config file > region default > existing US default,
so explicit API_URL/APP_URL-style overrides keep working unchanged.

- config.py: region registry + runtime resolve_url()/get_effective_region();
  EU maps api/app to api,app.roboflow.eu, detection/instance-seg to
  serverless.roboflow.eu, dedicated deployments to eu.roboflow.cloud.
  Universe and semantic-seg stay global .com (no EU instance today).
- login(): browser auth flow prints and validates the token against the
  region's app host at call time; forced re-login now merges the prior
  config instead of clobbering it (preserves region + URL overrides).
- CLI: auth login --region {us,eu} (interactive + --api-key paths),
  auth set-region, region + effective URLs in auth status (text/JSON),
  root login alias forwards --region.
- Docs: CLI-COMMANDS.md region section, README 'Using Roboflow EU'.
- Tests: back-compat guard (no region = byte-identical US defaults),
  precedence matrix, EU map, login persistence/merge, CLI paths.

INF-314
@imbgar-roboflow
imbgar-roboflow force-pushed the brandongarcia/inf-314-cli-eu-region branch from 0e1d22a to 6ad02bc Compare July 31, 2026 18:46
@hansent
hansent self-requested a review August 10, 2026 16:40

@imbgar-roboflow imbgar-roboflow left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TL;DR

Needs work — 6 fixes before merge · 5 follow-ups · 1 question.

Required before merge

  • Fix before merge: [sol-ultra] New annotations make the package unimportable on advertised Python versions — roboflow/config.py:29
    Exact replacement attached as an inline suggestion.
  • Fix before merge: [sol-ultra] The advertised login command cannot switch an existing user to EU — roboflow/cli/handlers/auth.py:242
    Exact replacement attached as an inline suggestion.
  • Fix before merge: [sol-ultra] Programmatic EU login leaves the SDK on its import-time API endpoint — roboflow/__init__.py:80
    Exact replacement attached as an inline suggestion.
  • Fix before merge: [sol-ultra] Invalid regions corrupt JSON-mode stderr — roboflow/config.py:76
    Exact replacement attached as an inline suggestion.
  • Fix before merge: [sol-ultra] Public URL helpers still reject EU application URLs — README.md:100
    Exact replacement attached as an inline suggestion.
  • Fix before merge: [fable-xhigh] set-region always warns about stored credentials, even when none exist or region is unchanged — roboflow/cli/handlers/auth.py:360
    Exact replacement attached as an inline suggestion.

Follow-ups

  • [fable-xhigh] Region allow-list is duplicated in three places with inconsistent normalization — roboflow/__init__.py:72
  • [fable-xhigh] Forced re-login still deletes the config before the token exchange, defeating the new merge on failure — roboflow/__init__.py:105
  • [fable-xhigh] Not-logged-in JSON path bypasses output_error and diverges from text mode — roboflow/cli/handlers/auth.py:309
  • [fable-xhigh] Stored credentials are not region-scoped; US and EU workspaces share one map and one global region — roboflow/cli/handlers/auth.py:208
  • [fable-xhigh] README/login docs omit that roboflow.login(region=...) does not affect same-session SDK constants — README.md:109

Questions

  • [fable-xhigh] Should an explicit --region lose silently to an API_URL/APP_URL env override? — roboflow/config.py:98

Could not be verified

Raised by the congregation but neither confirmed nor refuted — the review context lacked the evidence. Treat as open questions, not clearances.

  • tests/cli/test_auth_region.py:161 [fable-xhigh] Test reads result.stderr from a default CliRunner, which requires Click >= 8.2 — Cross-examination verdict was uncertain with voter-a=refute and voter-b=confirm; the adversarial layer did not confirm this finding, so it is rejected per the rule that only confirmed findings may be accepted.

How this verdict was reached

  • Jury: superduper-trio (reviewer) · superduper-adversarial (cross_examiner) · superduperduper-glm (adjudicator)
  • Work: 18 raised · 17 accepted · 0 refuted · 1 unverifiable · 6 blocking
  • Verifier: ran on blocking candidates
  • Withheld: 4 take-or-leave notes (below the public evidence bar)
  • Coverage: superduper-trio reviewed 9/9 files and 26/26 hunks · superduper-adversarial independently reviewed 9/9 files and 26/26 hunks

Failure architecture

Why does importing roboflow fail on Python 3.8, which the README still advertises?

config.py evaluates set[str] at module import without postponed-annotation import; Python 3.8 lacks set.class_getitem, so the subscript raises TypeError before any region logic runs.

flowchart LR
  D1N1["source: README advertises Python 3.8 or higher known"]
  D1N2["component: config.py line 29 uses set str at module level with no __future__ import known"]
  D1N3["decision: Python 3.8 evaluates set str as runtime subscript (no class_getitem) known"]
  D1N4["failure: TypeError at import: roboflow unimportable on advertised Python 3.8 known"]
  D1N1 -->|"3.8 users install per README known"| D1N2
  D1N2 -->|"no postponed annotations; subscript evaluated at runtime known"| D1N3
  D1N3 -->|"set.__class_getitem__ absent in 3.8 raises TypeError known"| D1N4
Loading

Scope: roboflow/config.py module import on Python 3.8
Assumptions: str | None in resolve_url would also fail on 3.9, but set[str] fails first at module import
Unresolved: Exact Python 3.8 traceback not captured in supplied diff; failure inferred from language semantics stated in finding

Why does roboflow.login(region='eu') not route subsequent SDK operations to the EU API?

API_URL is bound by value at import time; login only dynamically resolves APP_URL and persists region, so check_key and Roboflow() keep using the stale US API_URL with the newly stored EU key.

flowchart LR
  D2N1["source: Process imports roboflow; API_URL bound by value at import time known"]
  D2N2["component: login(region 'eu') dynamically resolves only APP_URL, persists EU region known"]
  D2N3["component: check_key and Roboflow() use stale API_URL constant known"]
  D2N4["failure: EU key submitted to US API endpoint until process restart known"]
  D2N1 -->|"API_URL already bound before login runs known"| D2N2
  D2N2 -->|"login does not re-resolve API_URL known"| D2N3
  D2N3 -->|"stale US endpoint used with EU credentials known"| D2N4
Loading

Scope: roboflow/init.py import-time binding vs runtime login
Assumptions: Restarting the process would re-import and re-bind API_URL to the persisted EU region
Unresolved: Exact line where API_URL is imported by value not isolated beyond finding anchor; check_key implementation details not shown in diff

How does an invalid ROBOFLOW_REGION corrupt --json output on stderr?

_normalize_region prints a plain-text warning to stderr during import-time init; _status then writes its JSON error to the same stream, so consumers receive a warning line followed by JSON instead of one parseable document.

flowchart LR
  D3N1["source: ROBOFLOW_REGION set to invalid value; --json mode requested known"]
  D3N2["component: _normalize_region prints plain-text warning to stderr during import-time init known"]
  D3N3["component: _status writes JSON error to same stderr stream known"]
  D3N4["failure: Consumer receives warning line + JSON, not one parseable document known"]
  D3N1 -->|"invalid region triggers unconditional warning known"| D3N2
  D3N2 -->|"warning precedes JSON on same stderr stream known"| D3N3
  D3N3 -->|"mixed-format stderr breaks JSON parsing known"| D3N4
Loading

_Scope: stderr stream shared between _normalize_region warning and status JSON output
_Assumptions: No credentials are present in the scenario, so status produces an error JSON rather than a success payload
_Unresolved: Exact status output line in auth.py not isolated; stdout vs stderr separation for other commands not analyzed

Reviewed commit 6ad02bcf67357433fb2348dfdce3659ba0f429fd.

Comment thread README.md
```

### Using Roboflow EU

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fix_before_merge · api · 92% — [sol-ultra] Public URL helpers still reject EU application URLs

The new README states that the same package supports the EU platform, but the supplied complete roboflow/init.py still permits only literal app.roboflow.com or universe.roboflow.com URLs in both load_model and download_dataset. An app.roboflow.eu model or dataset URL is rejected before parsing, even when the effective region is EU.

Why this matters — regional-url-compatibility: Public helpers that accept platform URLs must recognize the corresponding hostname of every region the package claims to support.

Comment thread roboflow/__init__.py
# the module-level URL constants were resolved when roboflow was imported.
from roboflow.config import resolve_url

app_url = resolve_url("APP_URL", region=normalized_region)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fix_before_merge · bug · 96% — [sol-ultra] Programmatic EU login leaves the SDK on its import-time API endpoint

login(region="eu") dynamically resolves only APP_URL and persists the region after authentication. The same module imported API_URL by value before login, and check_key plus subsequent Roboflow operations continue using that stale constant. Thus a notebook or process that calls roboflow.login(region="eu") and then constructs roboflow.Roboflow() can submit the newly stored EU key to the US API until the process is restarted.

Why this matters — runtime-region-consistency: Changing authentication region within a process must update every endpoint used by the immediately following SDK operations.

return

roboflow.login(workspace=workspace_id, force=force)
roboflow.login(workspace=workspace_id, force=force, region=region)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fix_before_merge · bug · 98% — [sol-ultra] The advertised login command cannot switch an existing user to EU

_login validates --region, but its interactive branch returns early whenever the config file already exists and --force is false. Consequently, an existing US user running the documented roboflow auth login --region eu never reaches the changed call, opens no EU authentication flow, and does not persist EU; it only reports that the user is already logged in.

Why this matters — explicit-option-must-take-effect: A newly documented explicit selection option must not silently become a no-op in the primary migration scenario.

effective_region = get_effective_region()
api_url = resolve_url("API_URL")
app_url = resolve_url("APP_URL")
warning = (

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fix_before_merge · bug · 65% — [fable-xhigh] set-region always warns about stored credentials, even when none exist or region is unchanged

roboflow/cli/handlers/auth.py:360-364 builds warning unconditionally and emits it in both text (line 379) and JSON (line 377). previous_region (line 351) is the effective region before the save. Running auth set-region us when already on us, or on an empty config (exactly what tests/cli/test_auth_region.py:156 does before any login), tells the user 'Stored credentials were issued by the previously configured US platform ... login --force ... may be needed', which is false in both cases. When ROBOFLOW_REGION is set in the environment, 'previously configured' also reflects the env value rather than the saved one. For --json consumers the always-present warning key becomes noise. Gate the message on previous_region != region and config.get('workspaces') and only include the key when it applies; test_set_region_then_status_shows_eu_endpoints (which asserts the warning) would still pass since it has stored credentials and switches us->eu.

Why this matters — warn-only-when-actionable: A warning that fires regardless of state is misleading in the false-positive cases and trains users and agents to ignore it when it matters.

Comment thread roboflow/config.py
}

_UNSET = object()
_WARNED_UNKNOWN_REGIONS: set[str] = set()

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fix_before_merge · compat · 99% — [sol-ultra] New annotations make the package unimportable on advertised Python versions

roboflow/config.py has no postponed-annotation import, but now evaluates set[str] at module import and later evaluates str | None in resolve_url. set[str] fails on Python 3.8, while str | None fails on Python 3.8 and 3.9. The supplied README still advertises Python 3.8 or higher, so importing roboflow will fail before any region logic runs on supported installations.

Why this matters — supported-python-importability: Code imported at package initialization must remain syntactically and semantically importable on every advertised Python version.

Comment thread roboflow/config.py
normalized_region = region.strip().lower() if isinstance(region, str) else ""
if normalized_region in REGION_URL_DEFAULTS:
return normalized_region

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fix_before_merge · api · 95% — [sol-ultra] Invalid regions corrupt JSON-mode stderr

_normalize_region prints a plain-text warning directly to stderr, including during import-time URL initialization. For a command such as ROBOFLOW_REGION=bogus roboflow --json auth status with no credentials, _status subsequently writes its JSON error to the same stderr stream. Consumers receive a warning line followed by JSON rather than one parseable JSON document.

Why this matters — json-output-integrity: Machine-readable mode must not mix unstructured diagnostics into the stream designated for structured errors.

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