Skip to content

Commit 41e47a5

Browse files
rvirani1claude
andcommitted
feat: add roboflow-slim package for lightweight installs (ENT-1082)
Publishes a second PyPI package "roboflow-slim" from the same codebase with only lightweight dependencies (no opencv, numpy, matplotlib, Pillow). Supports vision events, workspace management, auth, and CLI. - Guard heavy imports (Project, Workspace, models) with try/except - Make image_utils a lazy import in rfapi.upload_image - Move PIL import into two_stage/two_stage_ocr methods - Add requirements-slim.txt, setup_slim.py, publish-slim CI job - Add test-slim CI job and slim compat tests pip install roboflow is completely unchanged. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent d298418 commit 41e47a5

9 files changed

Lines changed: 239 additions & 6 deletions

File tree

.github/workflows/publish.yml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,29 @@ jobs:
2828
run: |
2929
make publish -e PYPI_USERNAME=$PYPI_USERNAME -e PYPI_PASSWORD=$PYPI_PASSWORD -e PYPI_TEST_PASSWORD=$PYPI_TEST_PASSWORD
3030
31+
build-slim:
32+
needs: build
33+
runs-on: ubuntu-latest
34+
steps:
35+
- name: 🛎️ Checkout
36+
uses: actions/checkout@v4
37+
with:
38+
ref: ${{ github.head_ref }}
39+
- name: 🐍 Set up Python
40+
uses: actions/setup-python@v5
41+
with:
42+
python-version: '3.10'
43+
- name: 🦾 Install dependencies
44+
run: |
45+
python -m pip install --upgrade pip
46+
pip install ".[dev]"
47+
- name: 🚀 Publish roboflow-slim to PyPi
48+
env:
49+
PYPI_USERNAME: ${{ secrets.PYPI_USERNAME }}
50+
PYPI_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
51+
run: |
52+
make publish-slim -e PYPI_USERNAME=$PYPI_USERNAME -e PYPI_PASSWORD=$PYPI_PASSWORD
53+
3154
deploy-docs:
3255
needs: build
3356
runs-on: ubuntu-latest

.github/workflows/test.yml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,24 @@ jobs:
3535
make check_code_quality
3636
- name: 🧪 Run tests
3737
run: "python -m unittest"
38+
39+
test-slim:
40+
runs-on: ubuntu-latest
41+
steps:
42+
- name: 🛎️ Checkout
43+
uses: actions/checkout@v4
44+
with:
45+
ref: ${{ github.event.pull_request.head.ref }}
46+
repository: ${{ github.event.pull_request.head.repo.full_name }}
47+
- name: 🐍 Set up Python 3.10
48+
uses: actions/setup-python@v5
49+
with:
50+
python-version: '3.10'
51+
- name: 🦾 Install slim dependencies
52+
run: |
53+
python -m pip install --upgrade pip
54+
pip install -r requirements-slim.txt
55+
pip install -e . --no-deps
56+
pip install responses
57+
- name: 🧪 Run slim-compatible tests
58+
run: "python -m unittest tests.test_slim_compat tests.test_vision_events"

Makefile

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
.PHONY: style check_code_quality publish
1+
.PHONY: style check_code_quality publish publish-slim
22

33
export PYTHONPATH = .
44
check_dirs := roboflow
@@ -16,3 +16,9 @@ publish:
1616
python setup.py sdist bdist_wheel
1717
twine check dist/*
1818
twine upload dist/* -u ${PYPI_USERNAME} -p ${PYPI_PASSWORD} --verbose
19+
20+
publish-slim:
21+
rm -rf dist/ build/ *.egg-info
22+
python setup_slim.py sdist bdist_wheel
23+
twine check dist/*
24+
twine upload dist/* -u ${PYPI_USERNAME} -p ${PYPI_PASSWORD} --verbose

requirements-slim.txt

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
certifi
2+
idna
3+
requests
4+
urllib3>=1.26.6
5+
tqdm>=4.41.0
6+
PyYAML>=5.3.1
7+
requests_toolbelt
8+
filetype
9+
typer>=0.12.0
10+
python-dateutil
11+
python-dotenv
12+
six

roboflow/__init__.py

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,18 @@
1010

1111
from roboflow.adapters import rfapi
1212
from roboflow.config import API_URL, APP_URL, DEMO_KEYS, load_roboflow_api_key
13-
from roboflow.core.project import Project
14-
from roboflow.core.workspace import Workspace
15-
from roboflow.models import CLIPModel, GazeModel # noqa: F401
1613
from roboflow.util.general import write_line
1714

15+
try:
16+
from roboflow.core.project import Project
17+
from roboflow.core.workspace import Workspace
18+
from roboflow.models import CLIPModel, GazeModel # noqa: F401
19+
except ImportError:
20+
Project = None # type: ignore[assignment,misc]
21+
Workspace = None # type: ignore[assignment,misc]
22+
CLIPModel = None # type: ignore[assignment,misc]
23+
GazeModel = None # type: ignore[assignment,misc]
24+
1825
__version__ = "1.3.1"
1926

2027

@@ -226,6 +233,11 @@ def auth(self):
226233
return self
227234

228235
def workspace(self, the_workspace=None):
236+
if Workspace is None:
237+
raise ImportError(
238+
"Workspace requires additional dependencies. Install the full package: pip install roboflow"
239+
)
240+
229241
sys.stdout.write("\r" + "loading Roboflow workspace...")
230242
sys.stdout.write("\n")
231243
sys.stdout.flush()
@@ -250,6 +262,10 @@ def project(self, project_name, the_workspace=None):
250262
:param the_workspace workspace name
251263
:return project object
252264
"""
265+
if Project is None:
266+
raise ImportError(
267+
"Project requires additional dependencies. Install the full package: pip install roboflow"
268+
)
253269

254270
if the_workspace is None:
255271
if "/" in project_name:

roboflow/adapters/rfapi.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
from requests_toolbelt.multipart.encoder import MultipartEncoder
99

1010
from roboflow.config import API_URL, DEFAULT_BATCH_NAME, DEFAULT_JOB_NAME
11-
from roboflow.util import image_utils
1211

1312

1413
class RoboflowError(Exception):
@@ -294,6 +293,8 @@ def upload_image(
294293

295294
# If image is not a hosted image
296295
if not hosted_image:
296+
from roboflow.util import image_utils
297+
297298
image_name = os.path.basename(image_path)
298299
imgjpeg = image_utils.file2jpeg(image_path)
299300

roboflow/core/workspace.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
from typing import Any, Dict, Generator, List, Optional
1010

1111
import requests
12-
from PIL import Image
1312
from requests.exceptions import HTTPError
1413
from tqdm import tqdm
1514

@@ -176,6 +175,8 @@ def two_stage(
176175
# TODO: fix docs
177176
dict: a json obj containing the results of the second stage detection
178177
""" # noqa: E501 // docs
178+
from PIL import Image
179+
179180
results = []
180181

181182
# create PIL image for cropping
@@ -245,6 +246,8 @@ def two_stage_ocr(
245246
# TODO: fix docs
246247
dict: a json obj containing the results of the second stage detection
247248
""" # noqa: E501 // docs
249+
from PIL import Image
250+
248251
results = []
249252

250253
# create PIL image for cropping

setup_slim.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import re
2+
3+
import setuptools
4+
from setuptools import find_packages
5+
6+
with open("./roboflow/__init__.py") as f:
7+
content = f.read()
8+
_search_version = re.search(r'__version__\s*=\s*[\'"]([^\'"]*)[\'"]', content)
9+
assert _search_version
10+
version = _search_version.group(1)
11+
12+
13+
with open("README.md") as fh:
14+
long_description = fh.read()
15+
16+
with open("requirements-slim.txt") as fh:
17+
install_requires = fh.read().split("\n")
18+
19+
setuptools.setup(
20+
name="roboflow-slim",
21+
version=version,
22+
author="Roboflow",
23+
author_email="support@roboflow.com",
24+
description="Lightweight Roboflow SDK for vision events, workspace management, and CLI",
25+
long_description=long_description,
26+
long_description_content_type="text/markdown",
27+
url="https://github.com/roboflow-ai/roboflow-python",
28+
install_requires=install_requires,
29+
packages=find_packages(exclude=("tests",)),
30+
extras_require={
31+
"dev": [
32+
"mypy",
33+
"responses",
34+
"ruff",
35+
"twine",
36+
"types-pyyaml",
37+
"types-requests",
38+
"types-setuptools",
39+
"types-tqdm",
40+
"wheel",
41+
],
42+
},
43+
entry_points={
44+
"console_scripts": [
45+
"roboflow=roboflow.roboflowpy:main",
46+
],
47+
},
48+
classifiers=[
49+
"Programming Language :: Python :: 3",
50+
"License :: OSI Approved :: Apache Software License",
51+
"Operating System :: OS Independent",
52+
],
53+
python_requires=">=3.10",
54+
)

tests/test_slim_compat.py

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
"""Tests for slim install compatibility.
2+
3+
Verifies that the package can be imported and lightweight features work
4+
even when heavy dependencies (PIL, opencv, numpy, matplotlib) are missing.
5+
6+
In a full install, these tests verify the guards don't break normal behavior.
7+
In a slim install, they verify graceful degradation.
8+
"""
9+
10+
import unittest
11+
12+
13+
class TestSlimImport(unittest.TestCase):
14+
"""Verify that importing the package always succeeds."""
15+
16+
def test_import_roboflow(self):
17+
import roboflow
18+
19+
self.assertIsNotNone(roboflow.__version__)
20+
21+
def test_import_vision_events_adapter(self):
22+
from roboflow.adapters import vision_events_api
23+
24+
self.assertTrue(callable(vision_events_api.write_event))
25+
self.assertTrue(callable(vision_events_api.write_batch))
26+
self.assertTrue(callable(vision_events_api.query))
27+
self.assertTrue(callable(vision_events_api.list_use_cases))
28+
self.assertTrue(callable(vision_events_api.get_custom_metadata_schema))
29+
self.assertTrue(callable(vision_events_api.upload_image))
30+
31+
def test_import_config(self):
32+
from roboflow.config import API_URL
33+
34+
self.assertIsInstance(API_URL, str)
35+
36+
def test_import_rfapi(self):
37+
from roboflow.adapters.rfapi import RoboflowError
38+
39+
self.assertTrue(issubclass(RoboflowError, Exception))
40+
41+
def test_import_cli(self):
42+
from roboflow.cli import app
43+
44+
self.assertIsNotNone(app)
45+
46+
47+
class TestSlimGracefulDegradation(unittest.TestCase):
48+
"""Verify that heavy features fail with clear errors when deps are missing.
49+
50+
These tests only exercise the error path when PIL/opencv are absent.
51+
In a full install they verify the guard exists but doesn't fire.
52+
"""
53+
54+
def test_workspace_and_project_attributes_exist(self):
55+
"""Workspace and Project are either real classes or None sentinels."""
56+
import roboflow
57+
58+
# In full install these are classes; in slim they're None
59+
ws = roboflow.Workspace
60+
proj = roboflow.Project
61+
self.assertTrue(ws is None or callable(ws))
62+
self.assertTrue(proj is None or callable(proj))
63+
64+
def test_roboflow_workspace_guard(self):
65+
"""If Workspace is None (slim), calling workspace() raises ImportError."""
66+
import roboflow
67+
68+
if roboflow.Workspace is not None:
69+
self.skipTest("Full install, Workspace is available")
70+
71+
rf = roboflow.Roboflow.__new__(roboflow.Roboflow)
72+
rf.api_key = "test"
73+
rf.current_workspace = "test"
74+
rf.model_format = "yolov8"
75+
76+
with self.assertRaises(ImportError) as ctx:
77+
rf.workspace()
78+
self.assertIn("pip install roboflow", str(ctx.exception))
79+
80+
def test_roboflow_project_guard(self):
81+
"""If Project is None (slim), calling project() raises ImportError."""
82+
import roboflow
83+
84+
if roboflow.Project is not None:
85+
self.skipTest("Full install, Project is available")
86+
87+
rf = roboflow.Roboflow.__new__(roboflow.Roboflow)
88+
rf.api_key = "test"
89+
rf.current_workspace = "test"
90+
91+
with self.assertRaises(ImportError) as ctx:
92+
rf.project("test-project")
93+
self.assertIn("pip install roboflow", str(ctx.exception))
94+
95+
96+
if __name__ == "__main__":
97+
unittest.main()

0 commit comments

Comments
 (0)