Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 41 additions & 1 deletion CLI-COMMANDS.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,46 @@ export ROBOFLOW_API_KEY=rf_xxxxx # recommended for scripts and agents
roboflow auth login # or interactive login
```

### Select a Roboflow region

Roboflow uses the US platform by default. To authenticate with the EU
data-residency platform, select the region during login:

```bash
roboflow auth login --region eu
# The backwards-compatible alias accepts the same option:
roboflow login --region eu
```

The selection is saved in the Roboflow config file. You can change it later
or inspect the effective endpoints with:

```bash
roboflow auth set-region eu
roboflow auth status
```

For CI and other non-interactive environments, set `ROBOFLOW_REGION=eu`.
`ROBOFLOW_REGION` accepts `us` or `eu` (case-insensitive); an environment
value takes precedence over the saved region. Explicit per-URL environment or
config values such as `API_URL` continue to take precedence over the region.

| Endpoint | `us` (default) | `eu` |
|----------|----------------|------|
| API | `https://api.roboflow.com` | `https://api.roboflow.eu` |
| App / CLI authentication | `https://app.roboflow.com` | `https://app.roboflow.eu` |
| Object detection | `https://serverless.roboflow.com` | `https://serverless.roboflow.eu` |
| Instance segmentation | `https://serverless.roboflow.com` | `https://serverless.roboflow.eu` |
| Dedicated deployment | `https://roboflow.cloud` | `https://eu.roboflow.cloud` |
| Universe | `https://universe.roboflow.com` | `https://universe.roboflow.com` |
| Semantic segmentation | `https://segment.roboflow.com` | `https://segment.roboflow.com` |

Roboflow Universe remains a single global product, so its URL stays on
`.com` in the EU region. Semantic segmentation also remains on its current
`.com` endpoint. EU and US use separate authentication backends; obtain EU
API keys from `https://app.roboflow.eu` and log in again after switching if
your existing credentials were issued by the other region.

## Global flags

| Flag | Short | Description |
Expand Down Expand Up @@ -377,7 +417,7 @@ Version numbers are always numeric — that's how `x/y` is disambiguated between

| Command | Description |
|---------|-------------|
| `auth` | Login, logout, status, set default workspace |
| `auth` | Login, logout, status, set region or default workspace |
| `api-key` | List, create, update, protect, disable, revoke workspace API keys |
| `workspace` | List and inspect workspaces |
| `project` | List, get, create projects |
Expand Down
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,20 @@ import roboflow
roboflow.login()
```

### Using Roboflow EU

The same package supports Roboflow's EU data-residency platform. Select it
when logging in with the CLI:

```bash
roboflow auth login --region eu
```

For environment-based configuration and CI, set `ROBOFLOW_REGION=eu` before
running Python or CLI commands. EU and US use separate authentication
backends, so use an EU API key obtained from
[`app.roboflow.eu`](https://app.roboflow.eu).

<details>
<summary>Authenticate with an API key</summary>

Expand Down
41 changes: 32 additions & 9 deletions roboflow/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,19 @@ def check_key(api_key, model, notebook, num_retries=0):
return "onboarding"


def login(workspace=None, force=False):
def login(workspace=None, force=False, region=None):
normalized_region = None
if region is not None:
if not isinstance(region, str) or region.lower() not in {"us", "eu"}:
raise ValueError(f"Invalid region '{region}'. Expected one of: us, eu.")
normalized_region = region.lower()

# Resolve at call time so a region passed by the CLI is honored even though
# 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)

os_name = os.name

if os_name == "nt":
Expand All @@ -76,22 +88,31 @@ def login(workspace=None, force=False):

# default configuration location
conf_location = os.getenv("ROBOFLOW_CONFIG_DIR", default=default_path)
existing_config = {}
if os.path.isfile(conf_location) and not force:
write_line("You are already logged into Roboflow. To make a different login,run roboflow.login(force=True).")
return None
# we could eventually return the workspace object here
# return Roboflow().workspace()
elif os.path.isfile(conf_location) and force:
try:
with open(conf_location) as f:
existing_config = json.load(f)
except json.JSONDecodeError:
# A forced login has historically replaced an unreadable config.
existing_config = {}
if not isinstance(existing_config, dict):
existing_config = {}
os.remove(conf_location)

if workspace is None:
write_line("visit " + APP_URL + "/auth-cli to get your authentication token.")
write_line("visit " + app_url + "/auth-cli to get your authentication token.")
else:
write_line("visit " + APP_URL + "/auth-cli/?workspace=" + workspace + " to get your authentication token.")
write_line("visit " + app_url + "/auth-cli/?workspace=" + workspace + " to get your authentication token.")

token = getpass("Paste the authentication token here: ")

r_login = requests.get(APP_URL + "/query/cliAuthToken/" + token)
r_login = requests.get(app_url + "/query/cliAuthToken/" + token)

if r_login.status_code == 200:
r_login = r_login.json()
Expand All @@ -102,16 +123,18 @@ def login(workspace=None, force=False):
if not os.path.exists(os.path.dirname(conf_location)):
os.makedirs(os.path.dirname(conf_location))

r_login = {"workspaces": r_login}
existing_config["workspaces"] = r_login
# set first workspace as default workspace

default_workspace_id = list(r_login["workspaces"].keys())[0]
workspace = r_login["workspaces"][default_workspace_id]
r_login["RF_WORKSPACE"] = workspace["url"]
default_workspace_id = list(existing_config["workspaces"].keys())[0]
workspace = existing_config["workspaces"][default_workspace_id]
existing_config["RF_WORKSPACE"] = workspace["url"]
if normalized_region is not None:
existing_config["ROBOFLOW_REGION"] = normalized_region

# write config file
with open(conf_location, "w") as f:
json.dump(r_login, f, indent=2)
json.dump(existing_config, f, indent=2)

else:
r_login.raise_for_status()
Expand Down
5 changes: 4 additions & 1 deletion roboflow/cli/handlers/_aliases.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,15 @@ def login_alias(
login_api_key: Annotated[
Optional[str], typer.Option("--api-key", help="API key (skip interactive login)")
] = None,
region: Annotated[
Optional[str], typer.Option("--region", metavar="{us,eu}", help="Roboflow platform region")
] = None,
force: Annotated[bool, typer.Option("--force", "-f", help="Force re-login")] = False,
) -> None:
"""Log in to Roboflow (alias for 'auth login')."""
from roboflow.cli.handlers.auth import _login

args = ctx_to_args(ctx, login_api_key=login_api_key, force=force)
args = ctx_to_args(ctx, login_api_key=login_api_key, region=region, force=force)
_login(args)

@app.command("whoami", hidden=True)
Expand Down
Loading
Loading