Skip to content

Commit 0bf53e2

Browse files
committed
feat(deps): move boto3 to [aws] optional extra
boto3 and botocore removed from core dependencies and moved to a new [aws] optional extra, making provider SDK dependencies opt-in. - Add [aws] extra: boto3>=1.42.21, botocore>=1.42.21 - Add [all-providers] meta-extra (references [aws]) - Add [monitoring-aws] extra (AWS OTel boto instrumentation) - Add [test-aws] extra (moto + response mocking) - Remove opentelemetry-instrumentation-boto from [monitoring] - Update [all] to include [all-providers] - Update [dev] shim to include [all-providers] - Guard 3 module-level import leaks with try/except ImportError - Guard tests/conftest.py bare boto3 import; add AWS_AVAILABLE flag - Add auto-skip for AWS provider tests when [aws] not installed - Add architecture test for boto3 leak detection outside providers/aws/ - Add unit tests verifying core modules boot without [aws] extra - Bump version to 1.7.0 (breaking: boto3 no longer in core) - Update README with per-provider install matrix - Regenerate uv.lock
1 parent 7b5b9aa commit 0bf53e2

10 files changed

Lines changed: 2681 additions & 2289 deletions

File tree

CHANGELOG.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,33 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
66
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
77

88
<!-- insertion marker -->
9+
## [1.7.0] — Breaking Change
10+
11+
### Breaking Changes
12+
13+
- `boto3` and `botocore` removed from core `[project.dependencies]`.
14+
Install `orb-py[aws]` (or `orb-py[all]`) to restore the previous behaviour.
15+
Plain `pip install orb-py` no longer installs the AWS SDK.
16+
17+
### Added
18+
19+
- New `[aws]` extra: `boto3>=1.42.21`, `botocore>=1.42.21`.
20+
- New `[all-providers]` meta-extra: pulls in all currently implemented providers.
21+
- New `[monitoring-aws]` extra: AWS-specific OpenTelemetry boto instrumentation (previously bundled inside `[monitoring]`).
22+
- New `[test-aws]` extra: moto + response-mocking deps for AWS test suites.
23+
- Architecture test `test_boto3_leak_detection.py`: asserts boto3/botocore are never imported outside `providers/aws/`.
24+
- Unit tests `test_no_provider_install.py`: verifies ORB core modules boot cleanly when `[aws]` extra is absent.
25+
26+
### Changed
27+
28+
- `[monitoring]` extra no longer includes `opentelemetry-instrumentation-boto` (use `[monitoring-aws]`).
29+
- `[all]` extra now includes `[all-providers]` so `pip install orb-py[all]` still pulls everything.
30+
- `[dev]` shim extra now includes `[all-providers]` for full local development.
31+
- Three module-level import leaks fixed with `try/except ImportError` guards:
32+
`config/schemas/cleanup_schema.py`, `infrastructure/storage/registration.py`,
33+
`providers/registration.py` (deprecated shims).
34+
- `tests/conftest.py`: bare `import boto3` replaced with guarded `AWS_AVAILABLE` flag; AWS provider tests auto-skip when `[aws]` extra is absent.
35+
936
## Unreleased
1037

1138
<small>[Compare with latest](https://github.com/awslabs/open-resource-broker/compare/v0.1.0rc0...HEAD)</small>

README.md

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,12 +83,36 @@ Get ORB installed and configured for your environment.
8383
<details>
8484
<summary>Installation</summary>
8585

86-
### Standard install
86+
### Standard install (core only — no provider)
8787

8888
```bash
8989
pip install orb-py
9090
```
9191

92+
ORB boots cleanly with no provider registered. Any command that needs a provider
93+
will return a clear "no provider configured" error rather than an ImportError.
94+
95+
### Per-provider install
96+
97+
```bash
98+
pip install "orb-py[aws]" # AWS provider (boto3 + botocore)
99+
pip install "orb-py[aws,cli]" # AWS provider + colored CLI output
100+
pip install "orb-py[aws,api]" # AWS provider + REST API server
101+
pip install "orb-py[monitoring-aws]" # AWS provider + full monitoring stack
102+
pip install "orb-py[all]" # All providers + all features
103+
```
104+
105+
### Provider extras matrix
106+
107+
| Use case | Install command |
108+
|----------|----------------|
109+
| Core only (no provider) | `pip install orb-py` |
110+
| AWS operator | `pip install "orb-py[aws]"` |
111+
| AWS + colored CLI | `pip install "orb-py[aws,cli]"` |
112+
| AWS + REST API | `pip install "orb-py[aws,api]"` |
113+
| AWS + monitoring | `pip install "orb-py[monitoring-aws]"` |
114+
| Full (all providers + features) | `pip install "orb-py[all]"` |
115+
92116
### With colored CLI output
93117

94118
```bash

pyproject.toml

Lines changed: 45 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "orb-py"
7-
version = "1.6.2"
7+
version = "1.7.0"
88
description = "Open Resource Broker (ORB) — dynamic cloud resource provisioning via CLI and REST API"
99
readme = "README.md"
1010
license = "Apache-2.0"
@@ -42,9 +42,7 @@ classifiers = [
4242
requires-python = ">=3.10"
4343

4444
dependencies = [
45-
# Core dependencies (always needed)
46-
"boto3>=1.42.21",
47-
"botocore>=1.42.21",
45+
# Core dependencies (always needed, never provider-specific)
4846
"pydantic>=2.0.0",
4947
"pydantic-settings>=2.0.0",
5048
"sqlalchemy>=2.0.0",
@@ -58,6 +56,30 @@ dependencies = [
5856
]
5957

6058
[project.optional-dependencies]
59+
# ── Provider extras ────────────────────────────────────────────────────────
60+
# AWS provider — boto3 and botocore (moved from core in 1.7.0; breaking change)
61+
aws = [
62+
"boto3>=1.42.21",
63+
"botocore>=1.42.21",
64+
]
65+
66+
# Placeholder entries for future providers (add when provider is implemented)
67+
# azure = [
68+
# "azure-mgmt-compute>=30.0.0",
69+
# "azure-identity>=1.15.0",
70+
# "azure-mgmt-network>=25.0.0",
71+
# ]
72+
# gcp = [
73+
# "google-cloud-compute>=1.14.0",
74+
# "google-auth>=2.23.0",
75+
# ]
76+
77+
# Meta-extra: all currently implemented providers
78+
all-providers = [
79+
"orb-py[aws]",
80+
]
81+
82+
# ── Feature extras ─────────────────────────────────────────────────────────
6183
# CLI enhancements (colored output)
6284
cli = [
6385
"rich>=13.3.0",
@@ -72,20 +94,34 @@ api = [
7294
"jinja2>=3.1.0",
7395
]
7496

75-
# Monitoring & observability (for API/production deployments)
97+
# Monitoring & observability (provider-neutral — no boto3 instrumentation here)
7698
monitoring = [
7799
"opentelemetry-api>=1.20.0",
78100
"opentelemetry-sdk>=1.20.0",
79101
"opentelemetry-instrumentation-fastapi>=0.41b0",
80-
"opentelemetry-instrumentation-boto>=0.41b0",
81102
"opentelemetry-instrumentation-sqlalchemy>=0.41b0",
82103
"prometheus-client>=0.17.0",
83104
"psutil>=5.9.0",
84105
]
85106

86-
# All optional features
107+
# AWS-specific monitoring (boto3 instrumentation; requires [aws])
108+
monitoring-aws = [
109+
"orb-py[aws,monitoring]",
110+
"opentelemetry-instrumentation-boto>=0.41b0",
111+
]
112+
113+
# All optional features (providers + features)
87114
all = [
88-
"orb-py[cli,api,monitoring]",
115+
"orb-py[cli,api,monitoring,all-providers]",
116+
]
117+
118+
# ── Test extras ────────────────────────────────────────────────────────────
119+
# AWS test dependencies — moto + response mocking
120+
test-aws = [
121+
"orb-py[aws]",
122+
"moto[ec2,iam,ec2instanceconnect,autoscaling,ssm,dynamodb,sts,sqs,sns]>=5.1.19,<6.0.0",
123+
"responses>=0.24.0,<1.0.0",
124+
"requests-mock>=1.11.0,<2.0.0",
89125
]
90126

91127
# ---------------------------------------------------------------------------
@@ -151,7 +187,7 @@ ci = [
151187
"wheel>=0.41.3,<1.0.0",
152188
]
153189
dev = [
154-
"orb-py[ci]",
190+
"orb-py[ci,all-providers]",
155191
"virtualenv>=20.26.6",
156192
"pre-commit>=3.5.0,<5.0.0",
157193
"bump2version>=1.0.1,<2.0.0",
Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,17 @@
1-
"""Re-export shim — CleanupConfig has moved to orb.providers.aws.configuration.cleanup_config."""
1+
"""Re-export shim — CleanupConfig has moved to orb.providers.aws.configuration.cleanup_config.
22
3-
from orb.providers.aws.configuration.cleanup_config import (
4-
CleanupConfig,
5-
CleanupResourcesConfig,
6-
)
3+
Note: This shim imports from the AWS provider package. When the [aws] extra is not
4+
installed, CleanupConfig and CleanupResourcesConfig are set to None. Phase 2 of the
5+
provider-extras migration will invert this dependency so the models live in core.
6+
"""
7+
8+
try:
9+
from orb.providers.aws.configuration.cleanup_config import (
10+
CleanupConfig,
11+
CleanupResourcesConfig,
12+
)
13+
except ImportError:
14+
CleanupConfig = None # type: ignore[assignment,misc]
15+
CleanupResourcesConfig = None # type: ignore[assignment,misc]
716

817
__all__ = ["CleanupConfig", "CleanupResourcesConfig"]

src/orb/infrastructure/storage/registration.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,12 @@ def register_all_storage_types() -> None:
2222

2323
register_sql_storage()
2424

25-
from orb.providers.aws.storage.registration import register_dynamodb_storage
25+
try:
26+
from orb.providers.aws.storage.registration import register_dynamodb_storage
2627

27-
register_dynamodb_storage()
28+
register_dynamodb_storage()
29+
except ImportError:
30+
pass # [aws] extra not installed; DynamoDB storage unavailable
2831

2932

3033
def get_available_storage_types() -> list:

src/orb/providers/registration.py

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -91,10 +91,14 @@ def register_all_provider_cli_specs() -> None:
9191
``cli/args.py`` and other early-bootstrap callers continue to work.
9292
"""
9393
from orb.domain.base.ports.provider_cli_spec_port import CLISpecRegistry
94-
from orb.providers.aws.cli.aws_cli_spec import AWSCLISpec
9594

96-
if CLISpecRegistry.get("aws") is None:
97-
CLISpecRegistry.register("aws", AWSCLISpec())
95+
try:
96+
from orb.providers.aws.cli.aws_cli_spec import AWSCLISpec
97+
98+
if CLISpecRegistry.get("aws") is None:
99+
CLISpecRegistry.register("aws", AWSCLISpec())
100+
except ImportError:
101+
pass # [aws] extra not installed; AWS CLI spec unavailable
98102

99103

100104
def register_all_defaults_loaders() -> None:
@@ -112,9 +116,12 @@ def register_all_defaults_loaders() -> None:
112116
from orb.providers.registry.defaults_loader_registry import DefaultsLoaderRegistry
113117

114118
if DefaultsLoaderRegistry.get("aws") is None:
115-
from orb.providers.aws.defaults_loader import AWSDefaultsLoader
119+
try:
120+
from orb.providers.aws.defaults_loader import AWSDefaultsLoader
116121

117-
DefaultsLoaderRegistry.register("aws", AWSDefaultsLoader())
122+
DefaultsLoaderRegistry.register("aws", AWSDefaultsLoader())
123+
except ImportError:
124+
pass # [aws] extra not installed; AWS defaults loader unavailable
118125

119126

120127
def register_fallback_provider(

tests/conftest.py

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,9 +64,23 @@ def pytest_collection_modifyitems(config, items):
6464
skip_mocked = pytest.mark.skip(reason="moto tests skipped — remove --no-mocked to run")
6565
skip_provider = pytest.mark.skip(reason=f"filtered to --provider {provider_filter}")
6666

67+
# Auto-skip AWS provider tests when boto3 is not installed
68+
try:
69+
import boto3 as _boto3 # noqa: F401
70+
71+
aws_available = True
72+
except ImportError:
73+
aws_available = False
74+
75+
skip_no_aws = pytest.mark.skip(reason="[aws] extra not installed — boto3 unavailable")
76+
6777
for item in items:
6878
path = str(item.fspath)
6979

80+
# Auto-skip AWS provider tests when [aws] extra is not installed
81+
if not aws_available and "/providers/aws/" in path:
82+
item.add_marker(skip_no_aws)
83+
7084
# Provider filter
7185
if provider_filter and f"/providers/{provider_filter}" not in path:
7286
if "/providers/" in path:
@@ -122,7 +136,12 @@ def mock_aws(): # type: ignore[misc]
122136
yield
123137

124138

125-
import boto3
139+
try:
140+
import boto3
141+
142+
AWS_AVAILABLE = True
143+
except ImportError:
144+
AWS_AVAILABLE = False
126145

127146
from orb.config.manager import ConfigurationManager
128147
from orb.config.schemas.app_schema import AppConfig
@@ -286,18 +305,24 @@ def aws_mocks():
286305
@pytest.fixture
287306
def ec2_client(aws_mocks):
288307
"""Create a mocked EC2 client."""
308+
if not AWS_AVAILABLE:
309+
pytest.skip("boto3 not installed — install [aws] extra")
289310
return boto3.client("ec2", region_name="us-east-1")
290311

291312

292313
@pytest.fixture
293314
def autoscaling_client(aws_mocks):
294315
"""Create a mocked Auto Scaling client."""
316+
if not AWS_AVAILABLE:
317+
pytest.skip("boto3 not installed — install [aws] extra")
295318
return boto3.client("autoscaling", region_name="us-east-1")
296319

297320

298321
@pytest.fixture
299322
def ssm_client(aws_mocks):
300323
"""Create a mocked SSM client."""
324+
if not AWS_AVAILABLE:
325+
pytest.skip("boto3 not installed — install [aws] extra")
301326
return boto3.client("ssm", region_name="us-east-1")
302327

303328

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
"""Architecture test: boto3 and botocore must not be imported outside providers/aws/.
2+
3+
Importing boto3 or botocore directly in core or infrastructure modules couples the
4+
entire application to the [aws] extra even when it is not installed. All boto3/botocore
5+
usage must be confined to src/orb/providers/aws/.
6+
"""
7+
8+
from __future__ import annotations
9+
10+
from pathlib import Path
11+
12+
import pytest
13+
14+
from tests.unit.architecture.conftest import (
15+
EXCEPTION_PATHS,
16+
SRC_ORB,
17+
collect_python_files,
18+
extract_imports,
19+
)
20+
21+
_PROVIDERS_AWS_DIR = SRC_ORB / "providers" / "aws"
22+
23+
# All source files that live outside providers/aws/
24+
_NON_AWS_FILES = [
25+
f
26+
for f in collect_python_files(SRC_ORB)
27+
if not f.is_relative_to(_PROVIDERS_AWS_DIR) and str(f) not in EXCEPTION_PATHS
28+
]
29+
30+
# Top-level module names that constitute direct AWS SDK imports
31+
_AWS_SDK_MODULES = frozenset({"boto3", "botocore"})
32+
33+
# Known violations — files currently allowed to import boto3/botocore outside providers/aws/.
34+
# This set should remain empty; add entries only as a last resort with a tracking comment.
35+
_KNOWN_VIOLATIONS: frozenset[tuple[str, str]] = frozenset()
36+
37+
38+
@pytest.mark.parametrize(
39+
"filepath",
40+
_NON_AWS_FILES,
41+
ids=lambda p: str(p.relative_to(SRC_ORB)),
42+
)
43+
@pytest.mark.unit
44+
@pytest.mark.architecture
45+
def test_no_boto3_outside_aws_provider(filepath: Path) -> None:
46+
"""boto3/botocore imports must not appear outside src/orb/providers/aws/."""
47+
rel = str(filepath.relative_to(SRC_ORB))
48+
imports = extract_imports(filepath)
49+
new_violations = [
50+
imp
51+
for imp in imports
52+
if (imp in _AWS_SDK_MODULES or any(imp.startswith(f"{m}.") for m in _AWS_SDK_MODULES))
53+
and (rel, imp) not in _KNOWN_VIOLATIONS
54+
]
55+
assert new_violations == [], (
56+
f"{rel} imports boto3/botocore outside providers/aws/ — "
57+
f"move to providers/aws/ or guard with try/except ImportError. "
58+
f"Violations: {new_violations}"
59+
)

0 commit comments

Comments
 (0)