Skip to content
Merged
50 changes: 49 additions & 1 deletion CLI-COMMANDS.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,54 @@ 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.

For Roboflow staging, set `ROBOFLOW_ENVIRONMENT=staging` alongside the
region; it accepts `prod` (default) or `staging`, and anything else warns and
falls back to `prod`. It selects the `roboflow.one` (US) or `roboflow-eu.one`
(EU) hosts, matching `inference`. `roboflow auth status` reports both switches.

| 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` |
| Classification, keypoint, VLM (`SERVERLESS_URL`) | `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` | not available |

Roboflow Universe remains a single global product, so its URL stays on
`.com` in the EU region. Hosted semantic segmentation has no EU deployment, so
in the EU region it raises an error instead of sending images to the US
endpoint; set `SEMANTIC_SEGMENTATION_URL` explicitly to override. 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 @@ -498,7 +546,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
16 changes: 15 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ The Python package is documented on the [official Roboflow documentation site](h

## 💻 Installation

You will need to have `Python 3.8` or higher set up to use the Roboflow Python package.
You will need to have `Python 3.10` or higher set up to use the Roboflow Python package.

Run the following command to install the Roboflow Python package:

Expand Down Expand Up @@ -104,6 +104,20 @@ import roboflow
roboflow.login()
```

### Using Roboflow EU

Comment thread
imbgar-roboflow marked this conversation as resolved.
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
113 changes: 91 additions & 22 deletions roboflow/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,31 @@ 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):
from roboflow.config import (
CREDENTIALS_REGION_KEY,
SUPPORTED_REGIONS,
credentials_region,
get_effective_region,
has_credentials,
refresh_region_urls,
region_conflict,
resolve_url,
)

normalized_region = None
if region is not None:
if not isinstance(region, str) or region.lower() not in SUPPORTED_REGIONS:
raise ValueError(f"Invalid region '{region}'. Expected one of: {', '.join(SUPPORTED_REGIONS)}.")
normalized_region = region.lower()
conflicting = region_conflict(normalized_region)
if conflicting is not None:
# Requests would follow the environment, not the credentials issued here.
raise ValueError(
f"ROBOFLOW_REGION={conflicting} in the environment overrides region='{normalized_region}'. "
"Unset it or make it match before logging in."
)

os_name = os.name

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

# default configuration location
conf_location = os.getenv("ROBOFLOW_CONFIG_DIR", default=default_path)
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:
os.remove(conf_location)
existing_config = {}
if os.path.isfile(conf_location):
try:
with open(conf_location) as f:
existing_config = json.load(f)
except json.JSONDecodeError:
# A forced login has historically replaced an unreadable config.
if not force:
raise
existing_config = {}
if not isinstance(existing_config, dict):
existing_config = {}

# A config holding only preferences (e.g. from `auth set-region`) is not a session.
target_region = normalized_region or get_effective_region()
if has_credentials(existing_config) and not force:
if credentials_region(existing_config) == target_region:
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()
# Stored credentials were issued by the other platform and cannot authenticate here.
write_line(f"Stored credentials were issued by the {credentials_region(existing_config).upper()} platform.")

# 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.
app_url = resolve_url("APP_URL", region=normalized_region)

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 +148,24 @@ 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}
# The previous credentials are replaced only once the new ones are in hand.
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:
Comment thread
imbgar-roboflow marked this conversation as resolved.
existing_config["ROBOFLOW_REGION"] = normalized_region
existing_config[CREDENTIALS_REGION_KEY] = target_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)

if normalized_region is not None:
# Constants bound at import time still point at the previous region.
refresh_region_urls()
Comment thread
imbgar-roboflow marked this conversation as resolved.

else:
r_login.raise_for_status()
Expand Down Expand Up @@ -145,26 +199,41 @@ def initialize_roboflow(the_workspace=None):
return active_workspace


_ROBOFLOW_APP_HOSTS = (
"universe.roboflow.com",
"app.roboflow.com",
"app.roboflow.eu",
# Staging platforms from the endpoint matrix in roboflow.config.
"universe.roboflow.one",
"app.roboflow.one",
"app.roboflow-eu.one",
)


def _is_roboflow_app_url(url):
return any(host in url for host in _ROBOFLOW_APP_HOSTS)


def load_model(model_url):
"""High level function to load Roboflow models.

Args:
model_url: the model url to load.
Must be from either app.roboflow.com or universe.roboflow.com
Must be from app.roboflow.com, app.roboflow.eu or universe.roboflow.com

Returns:
the model object to use for inference
"""

operate_workspace = initialize_roboflow()

if "universe.roboflow.com" in model_url or "app.roboflow.com" in model_url:
if _is_roboflow_app_url(model_url):
parsed_url = urlparse(model_url)
path_parts = parsed_url.path.split("/")
project = path_parts[2]
version = int(path_parts[-1])
else:
raise ValueError("Model URL must be from either app.roboflow.com or universe.roboflow.com")
raise ValueError("Model URL must be from app.roboflow.com, app.roboflow.eu or universe.roboflow.com")

project = operate_workspace.project(project)
version = project.version(version)
Expand All @@ -179,22 +248,22 @@ def download_dataset(dataset_url, model_format, location=None):

Args:
dataset_url: the dataset url to download.
Must be from either app.roboflow.com or universe.roboflow.com
Must be from app.roboflow.com, app.roboflow.eu or universe.roboflow.com
model_format: the format the dataset will be downloaded in
location: the location the dataset will be downloaded to

Returns:
The dataset object with location available as dataset.location
"""

if "universe.roboflow.com" in dataset_url or "app.roboflow.com" in dataset_url:
if _is_roboflow_app_url(dataset_url):
parsed_url = urlparse(dataset_url)
path_parts = parsed_url.path.split("/")
project = path_parts[2]
version = int(path_parts[-1])
the_workspace = path_parts[1]
else:
raise ValueError("Model URL must be from either app.roboflow.com or universe.roboflow.com")
raise ValueError("Model URL must be from app.roboflow.com, app.roboflow.eu or universe.roboflow.com")
operate_workspace = initialize_roboflow(the_workspace=the_workspace)

project = operate_workspace.project(project)
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