├── .env.example ├── .github └── workflows │ ├── auto-tests.yml │ ├── code-check.yml │ ├── docs.yml │ └── python-publish.yml ├── .gitignore ├── .vscode └── tasks.json ├── Dockerfile ├── LICENSE ├── README.md ├── docs ├── api.md ├── concepts.md ├── examples.md ├── index.md ├── installation.md ├── settings.md └── usage.md ├── examples ├── assets │ ├── advertising.csv │ ├── dataset_code.txt │ └── iris.csv ├── async_example.py ├── big_upload_from_url.py ├── custom_factory.py.todo ├── docker_parallel_execution.py ├── file_conversion.py ├── getting_started.py ├── langchain_agent.py.todo ├── local_docker.py.todo ├── plot_dataset.py ├── simple_codeinterpreter.py.todo └── stream_chunk_timing.py ├── mkdocs.yml ├── pyproject.toml ├── requirements.lock ├── roadmap.todo ├── scripts ├── build.sh └── dev-setup.sh ├── src └── codeboxapi │ ├── __init__.py │ ├── api.py │ ├── codebox.py │ ├── docker.py │ ├── local.py │ ├── py.typed │ ├── remote.py │ ├── types.py │ └── utils.py └── tests ├── conftest.py ├── test_v01.py └── test_v02.py /.env.example: -------------------------------------------------------------------------------- 1 | # Local Optional, Required for Production Deployment 2 | # CODEBOX_API_KEY= 3 | 4 | # Logging (True/False) 5 | VERBOSE=True 6 | -------------------------------------------------------------------------------- /.github/workflows/auto-tests.yml: -------------------------------------------------------------------------------- 1 | name: 🔁 Pytest ⏳x60 2 | 3 | on: 4 | schedule: 5 | - cron: "*/60 * * * *" 6 | 7 | jobs: 8 | test: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - uses: actions/checkout@v2 13 | - uses: eifinger/setup-rye@v1 14 | with: 15 | python-version: 3.9 16 | enable_cache: true 17 | cache_prefix: "venv-codeboxapi" 18 | - run: rye sync 19 | - run: rye run pytest 20 | env: 21 | CODEBOX_API_KEY: ${{ secrets.CODEBOX_API_KEY }} 22 | -------------------------------------------------------------------------------- /.github/workflows/code-check.yml: -------------------------------------------------------------------------------- 1 | name: ☑️ CodeCheck 2 | 3 | on: [push] 4 | 5 | jobs: 6 | pre-commit: 7 | strategy: 8 | matrix: 9 | python-version: ["3.9", "3.10", "3.11", "3.12"] 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | - uses: eifinger/setup-rye@v1 14 | with: 15 | enable-cache: true 16 | cache-prefix: "venv-codeboxapi" 17 | - name: pin version 18 | run: rye pin ${{ matrix.python-version }} 19 | - name: Sync rye 20 | run: rye sync 21 | - name: Run tests 22 | env: 23 | CODEBOX_API_KEY: ${{ secrets.CODEBOX_API_KEY }} 24 | run: rye run pytest -m "not skip_on_actions" 25 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: docs 2 | on: 3 | push: 4 | branches: 5 | - main 6 | 7 | permissions: 8 | contents: write 9 | 10 | jobs: 11 | deploy: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v3 15 | - uses: actions/setup-python@v4 16 | with: 17 | python-version: 3.9 18 | - run: echo "cache_id=$(date --utc '+%V')" >> $GITHUB_ENV 19 | - uses: actions/cache@v3 20 | with: 21 | key: mkdocs-material-${{ env.cache_id }} 22 | path: .cache 23 | restore-keys: | 24 | mkdocs-material- 25 | - run: pip install mkdocs-material neoteroi-mkdocs 26 | - run: mkdocs gh-deploy --force 27 | -------------------------------------------------------------------------------- /.github/workflows/python-publish.yml: -------------------------------------------------------------------------------- 1 | name: ⬆️ to PyPi 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | permissions: 8 | contents: read 9 | 10 | jobs: 11 | deploy: 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v3 16 | - name: Set up Python 17 | uses: actions/setup-python@v3 18 | with: 19 | python-version: "3.9" 20 | - name: Install dependencies 21 | run: | 22 | python -m pip install --upgrade pip 23 | pip install build 24 | - name: Build package 25 | run: python -m build 26 | - name: Publish package 27 | uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29 28 | with: 29 | user: __token__ 30 | password: ${{ secrets.PYPI_API_TOKEN }} 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .codebox 2 | .env 3 | .DS_Store 4 | .pytest_cache 5 | *.pyc 6 | *.whl 7 | *.tar.gz 8 | site 9 | .python-version 10 | requirements-dev.lock 11 | .aider* 12 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "type": "shell", 6 | "command": "bash ./scripts/dev-setup.sh", 7 | "group": "build", 8 | "label": "dev-setup", 9 | "runOptions": { 10 | "runOn": "default" 11 | } 12 | } 13 | ] 14 | } -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ghcr.io/astral-sh/uv as uv 2 | 3 | FROM --platform=amd64 python:3.11-slim as build 4 | 5 | ENV VIRTUAL_ENV=/.venv PATH="/.venv/bin:$PATH" 6 | 7 | COPY --from=uv /uv /uv 8 | COPY README.md pyproject.toml src / 9 | 10 | RUN --mount=type=cache,target=/root/.cache/uv \ 11 | --mount=from=uv,source=/uv,target=/uv \ 12 | /uv venv /.venv && /uv pip install -e .[all] \ 13 | && rm -rf README.md pyproject.toml src 14 | 15 | # FROM --platform=amd64 python:3.11-slim as runtime 16 | 17 | ENV PORT=8069 18 | EXPOSE $PORT 19 | 20 | CMD ["/.venv/bin/python", "-m", "codeboxapi.api"] 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Dominic Bäumer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CodeBox 2 | 3 | [![Version](https://badge.fury.io/py/codeboxapi.svg)](https://badge.fury.io/py/codeboxapi) 4 | [![code-check](https://github.com/shroominic/codebox-api/actions/workflows/code-check.yml/badge.svg)](https://github.com/shroominic/codebox-api/actions/workflows/code-check.yml) 5 | ![Downloads](https://img.shields.io/pypi/dm/codeboxapi) 6 | ![License](https://img.shields.io/pypi/l/codeboxapi) 7 | 8 | CodeBox is the simplest cloud infrastructure for your LLM Apps and Services. 9 | It allows you to run python code in an isolated/sandboxed environment. 10 | 11 | ## Installation 12 | 13 | You can install CodeBox with pip: 14 | 15 | ```bash 16 | pip install codeboxapi 17 | ``` 18 | 19 | ## Usage 20 | 21 | ```bash 22 | export CODEBOX_API_KEY=local # for local development 23 | export CODEBOX_API_KEY=docker # for small projects 24 | export CODEBOX_API_KEY=sk-*************** # for production 25 | ``` 26 | 27 | ```python 28 | from codeboxapi import CodeBox 29 | 30 | # create a new codebox 31 | codebox = CodeBox() 32 | 33 | # run some code 34 | codebox.exec("a = 'Hello'") 35 | codebox.exec("b = 'World!'") 36 | codebox.exec("x = a + ', ' + b") 37 | result = codebox.exec("print(x)") 38 | 39 | print(result) 40 | # Hello, World! 41 | ``` 42 | 43 | ## Where to get your api-key? 44 | 45 | Checkout the [pricing page](https://codeboxapi.com/pricing) of CodeBoxAPI. By subscribing to a plan, 46 | you will receive an account with an api-key. 47 | Bear in mind, we don't have many automations set up right now, 48 | so you'll need to write an [email](mailto:team@codeboxapi.com) for things like refunds, 49 | sub cancellations, or upgrades. 50 | 51 | ## Docs 52 | 53 | Checkout the [documentation](https://shroominic.github.io/codebox-api/) for more details! 54 | 55 | ## Contributing 56 | 57 | Feel free to contribute to this project. 58 | You can open an issue or submit a pull request. 59 | 60 | ## License 61 | 62 | [MIT](https://choosealicense.com/licenses/mit/) 63 | 64 | ## Contact 65 | 66 | You can contact me at [team@codeboxapi.com](mailto:team@codeboxapi.com) 67 | -------------------------------------------------------------------------------- /docs/api.md: -------------------------------------------------------------------------------- 1 | [OAD(https://codeboxapi.com/openapi.json)] 2 | -------------------------------------------------------------------------------- /docs/concepts.md: -------------------------------------------------------------------------------- 1 | # Concepts 2 | 3 | ## Concepts Overview 4 | 5 | | name | description | 6 | |-|-| 7 | | BaseBox | Abstract base class for isolated code execution environments | 8 | | LocalBox | Local implementation of BaseBox for testing | 9 | | CodeBox | Remote API wrapper for executing code on CodeBox cloud service | 10 | | CodeBoxSettings | Configuration settings for the API client | 11 | | CodeBoxError | Custom exception class for API errors | 12 | 13 | ## BaseBox 14 | 15 | The BaseBox class is an abstract base class that defines the interface for isolated code execution environments, also called CodeBoxes. It contains abstract methods like `run`, `upload`, `download` etc. that all concrete CodeBox implementations need to implement. The BaseBox handles session management and enforces the core interface. 16 | 17 | BaseBox aims to provide a common interface to interact with any code execution sandbox. It is designed to be extended by subclasses for specific implementations like LocalBox or CodeBox. 18 | 19 | ## LocalBox 20 | 21 | LocalBox is a concrete implementation of BaseBox that runs code locally using a Jupyter kernel. It is intended for testing and development purposes. 22 | 23 | The key aspects are: 24 | 25 | - Spins up a local Jupyter kernel gateway to execute code 26 | - Implements all BaseBox methods like `run`, `upload` etc locally 27 | - Provides a sandboxed local environment without needing access to CodeBox cloud 28 | - Useful for testing code before deploying to CodeBox cloud 29 | 30 | LocalBox is the default CodeBox used when no API key is specified. It provides a seamless way to develop locally and then switch to the cloud. 31 | 32 | ## CodeBox 33 | 34 | CodeBox is the main class that provides access to the remote CodeBox cloud service. It handles: 35 | 36 | - Authentication using the API key 37 | - Making requests to the CodeBox API 38 | - Parsing responses and converting to common schema 39 | - Providing both sync and async access 40 | 41 | It extends BaseBox and implements all the core methods to execute code on the remote servers. 42 | 43 | The key methods are: 44 | 45 | - `start() / astart()` - Starts a new CodeBox instance 46 | - `stop() / astop()` - Stops and destroys a CodeBox instance 47 | - `run() / arun()` - Executes python code in the CodeBox 48 | - `upload() / aupload()` - Uploads a file into the CodeBox 49 | - `download() / adownload()` - Downloads a file from the CodeBox 50 | - `list_files() / alist_files()` - Lists all files in the CodeBox 51 | - `install() / ainstall()` - Installs a PyPI package into the CodeBox 52 | - `restart() / arestart()` - Restarts the Python kernel. This can be useful to clear state between executions. 53 | 54 | The CodeBox class provides a simple way to leverage the remote cloud infrastructure with minimal code changes. 55 | 56 | ## CodeBoxSettings 57 | 58 | This contains the client configuration like: 59 | 60 | - API key for authentication 61 | - Base URL for the API 62 | - Timeout values 63 | - Debug settings 64 | 65 | It loads values from environment variables and provides an easy way to configure the client. 66 | 67 | ## CodeBoxError 68 | 69 | Custom exception class raised when there is an error response from the API. It includes additional context like: 70 | 71 | - HTTP status code 72 | - Response JSON body 73 | - Response headers 74 | 75 | This provides an easy way to handle errors with additional context. 76 | -------------------------------------------------------------------------------- /docs/examples.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | ## Basic Usage 4 | 5 | Run code in a new CodeBox: 6 | 7 | ```python 8 | from codeboxapi import CodeBox 9 | 10 | with CodeBox() as codebox: 11 | print(codebox.status()) 12 | 13 | codebox.run("print('Hello World!')") 14 | ``` 15 | 16 | Run async code: 17 | 18 | ```python 19 | import asyncio 20 | from codeboxapi import CodeBox 21 | 22 | async def main(): 23 | async with CodeBox() as codebox: 24 | await codebox.astatus() 25 | await codebox.arun("print('Hello World!')") 26 | 27 | asyncio.run(main()) 28 | ``` 29 | 30 | ## File IO 31 | 32 | Upload and download files: 33 | 34 | ```python 35 | from codeboxapi import CodeBox 36 | 37 | with CodeBox() as codebox: 38 | 39 | # Upload file 40 | codebox.upload("data.csv", b"1,2,3\ 41 | 4,5,6") 42 | 43 | # List files 44 | print(codebox.list_files()) 45 | 46 | # Download file 47 | data = codebox.download("data.csv") 48 | print(data.content) 49 | ``` 50 | 51 | ## Package Installation 52 | 53 | Install packages into the CodeBox: 54 | 55 | ```python 56 | from codeboxapi import CodeBox 57 | 58 | with CodeBox() as codebox: 59 | 60 | # Install packages 61 | codebox.install("pandas") 62 | codebox.install("matplotlib") 63 | 64 | # Use them 65 | codebox.run("import pandas as pd") 66 | codebox.run("import matplotlib.pyplot as plt") 67 | ``` 68 | 69 | ## Restoring Sessions 70 | 71 | Restore a CodeBox session from its ID: 72 | 73 | ```python 74 | from codeboxapi import CodeBox 75 | 76 | # Start CodeBox and save ID 77 | codebox = CodeBox() 78 | codebox.start() 79 | session_id = codebox.session_id 80 | 81 | #delete session 82 | del session 83 | 84 | # Restore session 85 | codebox = CodeBox.from_id(session_id) 86 | print(codebox.status()) 87 | ``` 88 | 89 | ## Parallel Execution 90 | 91 | Run multiple CodeBoxes in parallel: 92 | 93 | ```python 94 | import asyncio 95 | from codeboxapi import CodeBox 96 | 97 | async def main(): 98 | await asyncio.gather( 99 | spawn_codebox() for _ in range(10) 100 | ) 101 | 102 | async def spawn_codebox(): 103 | async with CodeBox() as codebox: 104 | print(await codebox.arun("print('Hello World!')")) 105 | 106 | asyncio.run(main()) 107 | ``` 108 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | 2 | # 📦 CodeBox API 3 | 4 | [![Version](https://badge.fury.io/py/codeboxapi.svg)](https://badge.fury.io/py/codeboxapi) 5 | [![code-check](https://github.com/shroominic/codebox-api/actions/workflows/code-check.yml/badge.svg)](https://github.com/shroominic/codebox-api/actions/workflows/code-check.yml) 6 | ![Downloads](https://img.shields.io/pypi/dm/codeboxapi) 7 | ![License](https://img.shields.io/pypi/l/codeboxapi) 8 | ![PyVersion](https://img.shields.io/pypi/pyversions/codeboxapi) 9 | 10 | CodeBox is the simplest cloud infrastructure for running and testing python code in an isolated environment. It allows developers to execute arbitrary python code without worrying about security or dependencies. Some key features include: 11 | 12 | - Securely execute python code in a sandboxed container 13 | - Easily install python packages into the environment 14 | - Built-in file storage for uploading and downloading files 15 | - Support for running code asynchronously using asyncio 16 | - Local testing mode for development without an internet connection 17 | 18 | ## Why is SandBoxing important? 19 | 20 | When deploying LLM Agents to production, it is important to ensure 21 | that the code they run is safe and does not contain any malicious code. 22 | This is especially important when considering prompt injection, which 23 | could give an attacker access to the entire system. 24 | 25 | ## How does CodeBox work? 26 | 27 | CodeBox uses a cloud hosted system to run hardened containers 28 | that are designed to be secure. These containers are then used to 29 | run the code that is sent to the API. This ensures that the code 30 | is run in a secure environment, and that the code cannot access 31 | the host system. 32 | 33 | ## Links & Resources 34 | 35 | - [CodeInterpreterAPI](https://github.com/shroominic/codeinterpreter-api) 36 | - [Documentation](https://shroominic.github.io/codebox-api/) 37 | - [REST API](https://codeboxapi.com/docs) 38 | - [Get API Key](https://pay.codeboxapi.com/b/00g3e6dZX2fTg0gaEE) 39 | - [Github Repo](https://github.com/shroominic/codebox-api) 40 | - [PyPI Package](https://pypi.org/project/codeboxapi/) 41 | -------------------------------------------------------------------------------- /docs/installation.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | CodeBox can be installed via pip: 4 | 5 | ```bash 6 | pip install codeboxapi 7 | ``` 8 | 9 | This will install the `codeboxapi` package and all dependencies. 10 | 11 | For local development without an API key, you will also need to install `jupyter-kernel-gateway`: 12 | 13 | ```bash 14 | pip install "codeboxapi[all]" 15 | ``` 16 | -------------------------------------------------------------------------------- /docs/settings.md: -------------------------------------------------------------------------------- 1 | # Settings 2 | 3 | ## Settings Class Overview 4 | 5 | The configuration settings are encapsulated within the `CodeBoxSettings` class, which inherits from Pydantic's `BaseSettings` class. 6 | 7 | `codeboxapi/config.py` 8 | 9 | ```python 10 | class CodeBoxSettings(BaseSettings): 11 | ... 12 | ``` 13 | 14 | ## Setting Descriptions 15 | 16 | ### Logging Settings 17 | 18 | - `VERBOSE: bool = False` 19 | Determines if verbose logging is enabled or not. 20 | 21 | - `SHOW_INFO: bool = True` 22 | Determines if information-level logging is enabled. 23 | 24 | ### CodeBox API Settings 25 | 26 | - `CODEBOX_API_KEY: str | None = None` 27 | The API key for CodeBox. 28 | 29 | - `CODEBOX_BASE_URL: str = "https://codeboxapi.com/api/v1"` 30 | The base URL for the CodeBox API. 31 | 32 | - `CODEBOX_TIMEOUT: int = 20` 33 | Timeout for CodeBox API requests, in seconds. 34 | -------------------------------------------------------------------------------- /docs/usage.md: -------------------------------------------------------------------------------- 1 | # Usage 2 | 3 | To use CodeBox, you first need to obtain an API key from [the CodeBox website](https://pay.codeboxapi.com/b/00g3e6dZX2fTg0gaEE). 4 | 5 | You can then start a CodeBox session: 6 | 7 | ```python 8 | from codeboxapi import CodeBox 9 | 10 | with CodeBox() as codebox: 11 | codebox.run("a = 'Hello World!'") 12 | codebox.run("print(a)") 13 | ``` 14 | 15 | The context manager (`with * as *:`) will automatically start and shutdown the CodeBox. 16 | 17 | You can also use CodeBox asynchronously: 18 | 19 | ```python 20 | import asyncio 21 | from codeboxapi import CodeBox 22 | 23 | async def main(): 24 | async with CodeBox() as codebox: 25 | await codebox.astatus() 26 | await codebox.arun("print('Hello World!')") 27 | 28 | asyncio.run(main()) 29 | ``` 30 | 31 | ## CodeBox API Key 32 | 33 | If you want to use remote code execution, you will need to obtain an [API Key](https://pay.codeboxapi.com/b/00g3e6dZX2fTg0gaEE). This is nessesary for deployment when you care about security of your backend system and you want to serve multiple users in parallel. The CodeBox API Cloud service provides auto scaled infrastructure to run your code in a secure sandboxed environment. 34 | 35 | ## LocalBox 36 | 37 | If you just want to experiment local with this you dont need an api key and by not inserting one the system will automatically use a LocalBox in the background when you call the CodeBox() class. 38 | -------------------------------------------------------------------------------- /examples/assets/advertising.csv: -------------------------------------------------------------------------------- 1 | TV,Radio,Newspaper,Sales 2 | 230.1,37.8,69.2,22.1 3 | 44.5,39.3,45.1,10.4 4 | 17.2,45.9,69.3,12 5 | 151.5,41.3,58.5,16.5 6 | 180.8,10.8,58.4,17.9 7 | 8.7,48.9,75,7.2 8 | 57.5,32.8,23.5,11.8 9 | 120.2,19.6,11.6,13.2 10 | 8.6,2.1,1,4.8 11 | 199.8,2.6,21.2,15.6 12 | 66.1,5.8,24.2,12.6 13 | 214.7,24,4,17.4 14 | 23.8,35.1,65.9,9.2 15 | 97.5,7.6,7.2,13.7 16 | 204.1,32.9,46,19 17 | 195.4,47.7,52.9,22.4 18 | 67.8,36.6,114,12.5 19 | 281.4,39.6,55.8,24.4 20 | 69.2,20.5,18.3,11.3 21 | 147.3,23.9,19.1,14.6 22 | 218.4,27.7,53.4,18 23 | 237.4,5.1,23.5,17.5 24 | 13.2,15.9,49.6,5.6 25 | 228.3,16.9,26.2,20.5 26 | 62.3,12.6,18.3,9.7 27 | 262.9,3.5,19.5,17 28 | 142.9,29.3,12.6,15 29 | 240.1,16.7,22.9,20.9 30 | 248.8,27.1,22.9,18.9 31 | 70.6,16,40.8,10.5 32 | 292.9,28.3,43.2,21.4 33 | 112.9,17.4,38.6,11.9 34 | 97.2,1.5,30,13.2 35 | 265.6,20,0.3,17.4 36 | 95.7,1.4,7.4,11.9 37 | 290.7,4.1,8.5,17.8 38 | 266.9,43.8,5,25.4 39 | 74.7,49.4,45.7,14.7 40 | 43.1,26.7,35.1,10.1 41 | 228,37.7,32,21.5 42 | 202.5,22.3,31.6,16.6 43 | 177,33.4,38.7,17.1 44 | 293.6,27.7,1.8,20.7 45 | 206.9,8.4,26.4,17.9 46 | 25.1,25.7,43.3,8.5 47 | 175.1,22.5,31.5,16.1 48 | 89.7,9.9,35.7,10.6 49 | 239.9,41.5,18.5,23.2 50 | 227.2,15.8,49.9,19.8 51 | 66.9,11.7,36.8,9.7 52 | 199.8,3.1,34.6,16.4 53 | 100.4,9.6,3.6,10.7 54 | 216.4,41.7,39.6,22.6 55 | 182.6,46.2,58.7,21.2 56 | 262.7,28.8,15.9,20.2 57 | 198.9,49.4,60,23.7 58 | 7.3,28.1,41.4,5.5 59 | 136.2,19.2,16.6,13.2 60 | 210.8,49.6,37.7,23.8 61 | 210.7,29.5,9.3,18.4 62 | 53.5,2,21.4,8.1 63 | 261.3,42.7,54.7,24.2 64 | 239.3,15.5,27.3,20.7 65 | 102.7,29.6,8.4,14 66 | 131.1,42.8,28.9,16 67 | 69,9.3,0.9,11.3 68 | 31.5,24.6,2.2,11 69 | 139.3,14.5,10.2,13.4 70 | 237.4,27.5,11,18.9 71 | 216.8,43.9,27.2,22.3 72 | 199.1,30.6,38.7,18.3 73 | 109.8,14.3,31.7,12.4 74 | 26.8,33,19.3,8.8 75 | 129.4,5.7,31.3,11 76 | 213.4,24.6,13.1,17 77 | 16.9,43.7,89.4,8.7 78 | 27.5,1.6,20.7,6.9 79 | 120.5,28.5,14.2,14.2 80 | 5.4,29.9,9.4,5.3 81 | 116,7.7,23.1,11 82 | 76.4,26.7,22.3,11.8 83 | 239.8,4.1,36.9,17.3 84 | 75.3,20.3,32.5,11.3 85 | 68.4,44.5,35.6,13.6 86 | 213.5,43,33.8,21.7 87 | 193.2,18.4,65.7,20.2 88 | 76.3,27.5,16,12 89 | 110.7,40.6,63.2,16 90 | 88.3,25.5,73.4,12.9 91 | 109.8,47.8,51.4,16.7 92 | 134.3,4.9,9.3,14 93 | 28.6,1.5,33,7.3 94 | 217.7,33.5,59,19.4 95 | 250.9,36.5,72.3,22.2 96 | 107.4,14,10.9,11.5 97 | 163.3,31.6,52.9,16.9 98 | 197.6,3.5,5.9,16.7 99 | 184.9,21,22,20.5 100 | 289.7,42.3,51.2,25.4 101 | 135.2,41.7,45.9,17.2 102 | 222.4,4.3,49.8,16.7 103 | 296.4,36.3,100.9,23.8 104 | 280.2,10.1,21.4,19.8 105 | 187.9,17.2,17.9,19.7 106 | 238.2,34.3,5.3,20.7 107 | 137.9,46.4,59,15 108 | 25,11,29.7,7.2 109 | 90.4,0.3,23.2,12 110 | 13.1,0.4,25.6,5.3 111 | 255.4,26.9,5.5,19.8 112 | 225.8,8.2,56.5,18.4 113 | 241.7,38,23.2,21.8 114 | 175.7,15.4,2.4,17.1 115 | 209.6,20.6,10.7,20.9 116 | 78.2,46.8,34.5,14.6 117 | 75.1,35,52.7,12.6 118 | 139.2,14.3,25.6,12.2 119 | 76.4,0.8,14.8,9.4 120 | 125.7,36.9,79.2,15.9 121 | 19.4,16,22.3,6.6 122 | 141.3,26.8,46.2,15.5 123 | 18.8,21.7,50.4,7 124 | 224,2.4,15.6,16.6 125 | 123.1,34.6,12.4,15.2 126 | 229.5,32.3,74.2,19.7 127 | 87.2,11.8,25.9,10.6 128 | 7.8,38.9,50.6,6.6 129 | 80.2,0,9.2,11.9 130 | 220.3,49,3.2,24.7 131 | 59.6,12,43.1,9.7 132 | 0.7,39.6,8.7,1.6 133 | 265.2,2.9,43,17.7 134 | 8.4,27.2,2.1,5.7 135 | 219.8,33.5,45.1,19.6 136 | 36.9,38.6,65.6,10.8 137 | 48.3,47,8.5,11.6 138 | 25.6,39,9.3,9.5 139 | 273.7,28.9,59.7,20.8 140 | 43,25.9,20.5,9.6 141 | 184.9,43.9,1.7,20.7 142 | 73.4,17,12.9,10.9 143 | 193.7,35.4,75.6,19.2 144 | 220.5,33.2,37.9,20.1 145 | 104.6,5.7,34.4,10.4 146 | 96.2,14.8,38.9,12.3 147 | 140.3,1.9,9,10.3 148 | 240.1,7.3,8.7,18.2 149 | 243.2,49,44.3,25.4 150 | 38,40.3,11.9,10.9 151 | 44.7,25.8,20.6,10.1 152 | 280.7,13.9,37,16.1 153 | 121,8.4,48.7,11.6 154 | 197.6,23.3,14.2,16.6 155 | 171.3,39.7,37.7,16 156 | 187.8,21.1,9.5,20.6 157 | 4.1,11.6,5.7,3.2 158 | 93.9,43.5,50.5,15.3 159 | 149.8,1.3,24.3,10.1 160 | 11.7,36.9,45.2,7.3 161 | 131.7,18.4,34.6,12.9 162 | 172.5,18.1,30.7,16.4 163 | 85.7,35.8,49.3,13.3 164 | 188.4,18.1,25.6,19.9 165 | 163.5,36.8,7.4,18 166 | 117.2,14.7,5.4,11.9 167 | 234.5,3.4,84.8,16.9 168 | 17.9,37.6,21.6,8 169 | 206.8,5.2,19.4,17.2 170 | 215.4,23.6,57.6,17.1 171 | 284.3,10.6,6.4,20 172 | 50,11.6,18.4,8.4 173 | 164.5,20.9,47.4,17.5 174 | 19.6,20.1,17,7.6 175 | 168.4,7.1,12.8,16.7 176 | 222.4,3.4,13.1,16.5 177 | 276.9,48.9,41.8,27 178 | 248.4,30.2,20.3,20.2 179 | 170.2,7.8,35.2,16.7 180 | 276.7,2.3,23.7,16.8 181 | 165.6,10,17.6,17.6 182 | 156.6,2.6,8.3,15.5 183 | 218.5,5.4,27.4,17.2 184 | 56.2,5.7,29.7,8.7 185 | 287.6,43,71.8,26.2 186 | 253.8,21.3,30,17.6 187 | 205,45.1,19.6,22.6 188 | 139.5,2.1,26.6,10.3 189 | 191.1,28.7,18.2,17.3 190 | 286,13.9,3.7,20.9 191 | 18.7,12.1,23.4,6.7 192 | 39.5,41.1,5.8,10.8 193 | 75.5,10.8,6,11.9 194 | 17.2,4.1,31.6,5.9 195 | 166.8,42,3.6,19.6 196 | 149.7,35.6,6,17.3 197 | 38.2,3.7,13.8,7.6 198 | 94.2,4.9,8.1,14 199 | 177,9.3,6.4,14.8 200 | 283.6,42,66.2,25.5 201 | 232.1,8.6,8.7,18.4 202 | -------------------------------------------------------------------------------- /examples/assets/dataset_code.txt: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | import matplotlib.pyplot as plt 3 | 4 | df = pd.read_csv("iris.csv", header=None) 5 | df.columns = ["sepal_length", "sepal_width", "petal_length", "petal_width", "class"] 6 | 7 | # Create a color dictionary for each class label 8 | color_dict = {'Iris-setosa': 0, 'Iris-versicolor': 1, 'Iris-virginica': 2} 9 | 10 | # Map the class labels to numbers 11 | df['color'] = df['class'].map(color_dict) 12 | 13 | df.plot.scatter(x="sepal_length", y="sepal_width", c="color", colormap="viridis") 14 | plt.show() 15 | -------------------------------------------------------------------------------- /examples/assets/iris.csv: -------------------------------------------------------------------------------- 1 | SepalLengthCm,SepalWidthCm,PetalLengthCm,PetalWidthCm,Species 2 | 5.1,3.5,1.4,0.2,Iris-setosa 3 | 4.9,3,1.4,0.2,Iris-setosa 4 | 4.7,3.2,1.3,0.2,Iris-setosa 5 | 4.6,3.1,1.5,0.2,Iris-setosa 6 | 5,3.6,1.4,0.2,Iris-setosa 7 | 5.4,3.9,1.7,0.4,Iris-setosa 8 | 4.6,3.4,1.4,0.3,Iris-setosa 9 | 5,3.4,1.5,0.2,Iris-setosa 10 | 4.4,2.9,1.4,0.2,Iris-setosa 11 | 4.9,3.1,1.5,0.1,Iris-setosa 12 | 5.4,3.7,1.5,0.2,Iris-setosa 13 | 4.8,3.4,1.6,0.2,Iris-setosa 14 | 4.8,3,1.4,0.1,Iris-setosa 15 | 4.3,3,1.1,0.1,Iris-setosa 16 | 5.8,4,1.2,0.2,Iris-setosa 17 | 5.7,4.4,1.5,0.4,Iris-setosa 18 | 5.4,3.9,1.3,0.4,Iris-setosa 19 | 5.1,3.5,1.4,0.3,Iris-setosa 20 | 5.7,3.8,1.7,0.3,Iris-setosa 21 | 5.1,3.8,1.5,0.3,Iris-setosa 22 | 5.4,3.4,1.7,0.2,Iris-setosa 23 | 5.1,3.7,1.5,0.4,Iris-setosa 24 | 4.6,3.6,1,0.2,Iris-setosa 25 | 5.1,3.3,1.7,0.5,Iris-setosa 26 | 4.8,3.4,1.9,0.2,Iris-setosa 27 | 5,3,1.6,0.2,Iris-setosa 28 | 5,3.4,1.6,0.4,Iris-setosa 29 | 5.2,3.5,1.5,0.2,Iris-setosa 30 | 5.2,3.4,1.4,0.2,Iris-setosa 31 | 4.7,3.2,1.6,0.2,Iris-setosa 32 | 4.8,3.1,1.6,0.2,Iris-setosa 33 | 5.4,3.4,1.5,0.4,Iris-setosa 34 | 5.2,4.1,1.5,0.1,Iris-setosa 35 | 5.5,4.2,1.4,0.2,Iris-setosa 36 | 4.9,3.1,1.5,0.1,Iris-setosa 37 | 5,3.2,1.2,0.2,Iris-setosa 38 | 5.5,3.5,1.3,0.2,Iris-setosa 39 | 4.9,3.1,1.5,0.1,Iris-setosa 40 | 4.4,3,1.3,0.2,Iris-setosa 41 | 5.1,3.4,1.5,0.2,Iris-setosa 42 | 5,3.5,1.3,0.3,Iris-setosa 43 | 4.5,2.3,1.3,0.3,Iris-setosa 44 | 4.4,3.2,1.3,0.2,Iris-setosa 45 | 5,3.5,1.6,0.6,Iris-setosa 46 | 5.1,3.8,1.9,0.4,Iris-setosa 47 | 4.8,3,1.4,0.3,Iris-setosa 48 | 5.1,3.8,1.6,0.2,Iris-setosa 49 | 4.6,3.2,1.4,0.2,Iris-setosa 50 | 5.3,3.7,1.5,0.2,Iris-setosa 51 | 5,3.3,1.4,0.2,Iris-setosa 52 | 7,3.2,4.7,1.4,Iris-versicolor 53 | 6.4,3.2,4.5,1.5,Iris-versicolor 54 | 6.9,3.1,4.9,1.5,Iris-versicolor 55 | 5.5,2.3,4,1.3,Iris-versicolor 56 | 6.5,2.8,4.6,1.5,Iris-versicolor 57 | 5.7,2.8,4.5,1.3,Iris-versicolor 58 | 6.3,3.3,4.7,1.6,Iris-versicolor 59 | 4.9,2.4,3.3,1,Iris-versicolor 60 | 6.6,2.9,4.6,1.3,Iris-versicolor 61 | 5.2,2.7,3.9,1.4,Iris-versicolor 62 | 5,2,3.5,1,Iris-versicolor 63 | 5.9,3,4.2,1.5,Iris-versicolor 64 | 6,2.2,4,1,Iris-versicolor 65 | 6.1,2.9,4.7,1.4,Iris-versicolor 66 | 5.6,2.9,3.6,1.3,Iris-versicolor 67 | 6.7,3.1,4.4,1.4,Iris-versicolor 68 | 5.6,3,4.5,1.5,Iris-versicolor 69 | 5.8,2.7,4.1,1,Iris-versicolor 70 | 6.2,2.2,4.5,1.5,Iris-versicolor 71 | 5.6,2.5,3.9,1.1,Iris-versicolor 72 | 5.9,3.2,4.8,1.8,Iris-versicolor 73 | 6.1,2.8,4,1.3,Iris-versicolor 74 | 6.3,2.5,4.9,1.5,Iris-versicolor 75 | 6.1,2.8,4.7,1.2,Iris-versicolor 76 | 6.4,2.9,4.3,1.3,Iris-versicolor 77 | 6.6,3,4.4,1.4,Iris-versicolor 78 | 6.8,2.8,4.8,1.4,Iris-versicolor 79 | 6.7,3,5,1.7,Iris-versicolor 80 | 6,2.9,4.5,1.5,Iris-versicolor 81 | 5.7,2.6,3.5,1,Iris-versicolor 82 | 5.5,2.4,3.8,1.1,Iris-versicolor 83 | 5.5,2.4,3.7,1,Iris-versicolor 84 | 5.8,2.7,3.9,1.2,Iris-versicolor 85 | 6,2.7,5.1,1.6,Iris-versicolor 86 | 5.4,3,4.5,1.5,Iris-versicolor 87 | 6,3.4,4.5,1.6,Iris-versicolor 88 | 6.7,3.1,4.7,1.5,Iris-versicolor 89 | 6.3,2.3,4.4,1.3,Iris-versicolor 90 | 5.6,3,4.1,1.3,Iris-versicolor 91 | 5.5,2.5,4,1.3,Iris-versicolor 92 | 5.5,2.6,4.4,1.2,Iris-versicolor 93 | 6.1,3,4.6,1.4,Iris-versicolor 94 | 5.8,2.6,4,1.2,Iris-versicolor 95 | 5,2.3,3.3,1,Iris-versicolor 96 | 5.6,2.7,4.2,1.3,Iris-versicolor 97 | 5.7,3,4.2,1.2,Iris-versicolor 98 | 5.7,2.9,4.2,1.3,Iris-versicolor 99 | 6.2,2.9,4.3,1.3,Iris-versicolor 100 | 5.1,2.5,3,1.1,Iris-versicolor 101 | 5.7,2.8,4.1,1.3,Iris-versicolor 102 | 6.3,3.3,6,2.5,Iris-virginica 103 | 5.8,2.7,5.1,1.9,Iris-virginica 104 | 7.1,3,5.9,2.1,Iris-virginica 105 | 6.3,2.9,5.6,1.8,Iris-virginica 106 | 6.5,3,5.8,2.2,Iris-virginica 107 | 7.6,3,6.6,2.1,Iris-virginica 108 | 4.9,2.5,4.5,1.7,Iris-virginica 109 | 7.3,2.9,6.3,1.8,Iris-virginica 110 | 6.7,2.5,5.8,1.8,Iris-virginica 111 | 7.2,3.6,6.1,2.5,Iris-virginica 112 | 6.5,3.2,5.1,2,Iris-virginica 113 | 6.4,2.7,5.3,1.9,Iris-virginica 114 | 6.8,3,5.5,2.1,Iris-virginica 115 | 5.7,2.5,5,2,Iris-virginica 116 | 5.8,2.8,5.1,2.4,Iris-virginica 117 | 6.4,3.2,5.3,2.3,Iris-virginica 118 | 6.5,3,5.5,1.8,Iris-virginica 119 | 7.7,3.8,6.7,2.2,Iris-virginica 120 | 7.7,2.6,6.9,2.3,Iris-virginica 121 | 6,2.2,5,1.5,Iris-virginica 122 | 6.9,3.2,5.7,2.3,Iris-virginica 123 | 5.6,2.8,4.9,2,Iris-virginica 124 | 7.7,2.8,6.7,2,Iris-virginica 125 | 6.3,2.7,4.9,1.8,Iris-virginica 126 | 6.7,3.3,5.7,2.1,Iris-virginica 127 | 7.2,3.2,6,1.8,Iris-virginica 128 | 6.2,2.8,4.8,1.8,Iris-virginica 129 | 6.1,3,4.9,1.8,Iris-virginica 130 | 6.4,2.8,5.6,2.1,Iris-virginica 131 | 7.2,3,5.8,1.6,Iris-virginica 132 | 7.4,2.8,6.1,1.9,Iris-virginica 133 | 7.9,3.8,6.4,2,Iris-virginica 134 | 6.4,2.8,5.6,2.2,Iris-virginica 135 | 6.3,2.8,5.1,1.5,Iris-virginica 136 | 6.1,2.6,5.6,1.4,Iris-virginica 137 | 7.7,3,6.1,2.3,Iris-virginica 138 | 6.3,3.4,5.6,2.4,Iris-virginica 139 | 6.4,3.1,5.5,1.8,Iris-virginica 140 | 6,3,4.8,1.8,Iris-virginica 141 | 6.9,3.1,5.4,2.1,Iris-virginica 142 | 6.7,3.1,5.6,2.4,Iris-virginica 143 | 6.9,3.1,5.1,2.3,Iris-virginica 144 | 5.8,2.7,5.1,1.9,Iris-virginica 145 | 6.8,3.2,5.9,2.3,Iris-virginica 146 | 6.7,3.3,5.7,2.5,Iris-virginica 147 | 6.7,3,5.2,2.3,Iris-virginica 148 | 6.3,2.5,5,1.9,Iris-virginica 149 | 6.5,3,5.2,2,Iris-virginica 150 | 6.2,3.4,5.4,2.3,Iris-virginica 151 | 5.9,3,5.1,1.8,Iris-virginica 152 | -------------------------------------------------------------------------------- /examples/async_example.py: -------------------------------------------------------------------------------- 1 | from codeboxapi import CodeBox 2 | 3 | codebox = CodeBox() 4 | 5 | 6 | async def async_examples(): 7 | # 1. Async Code Execution 8 | result = await codebox.aexec("print('Async Hello!')") 9 | print(result.text) 10 | 11 | # 2. Async File Operations 12 | await codebox.aupload("async_file.txt", b"Async content") 13 | 14 | downloaded = await codebox.adownload("async_file.txt") 15 | print("File content:", downloaded.get_content()) 16 | 17 | # 3. All Sync Methods are also available Async 18 | await codebox.ainstall("requests") 19 | 20 | # 4. Async Streaming 21 | async for chunk in codebox.astream_exec(""" 22 | for i in range(3): 23 | print(f"Async chunk {i}") 24 | import time 25 | time.sleep(1) 26 | """): 27 | print(chunk.content, end="") 28 | 29 | # 5. Async Streaming Download 30 | async for chunk in codebox.astream_download("async_file.txt"): 31 | assert isinstance(chunk, bytes) 32 | print(chunk.decode()) 33 | 34 | 35 | if __name__ == "__main__": 36 | import asyncio 37 | 38 | asyncio.run(async_examples()) 39 | -------------------------------------------------------------------------------- /examples/big_upload_from_url.py: -------------------------------------------------------------------------------- 1 | from codeboxapi import CodeBox 2 | 3 | 4 | def url_upload(codebox: CodeBox, url: str) -> None: 5 | codebox.exec( 6 | """ 7 | import requests 8 | 9 | def download_file_from_url(url: str) -> None: 10 | response = requests.get(url, stream=True) 11 | response.raise_for_status() 12 | file_name = url.split('/')[-1] 13 | with open('./' + file_name, 'wb') as file: 14 | for chunk in response.iter_content(chunk_size=8192): 15 | if chunk: 16 | file.write(chunk) 17 | """ 18 | ) 19 | print(codebox.exec(f"download_file_from_url('{url}')")) 20 | 21 | 22 | codebox = CodeBox() 23 | 24 | url_upload( 25 | codebox, 26 | "https://codeboxapistorage.blob.core.windows.net/bucket/data-test.arrow", 27 | ) 28 | print(codebox.list_files()) 29 | 30 | url_upload( 31 | codebox, 32 | "https://codeboxapistorage.blob.core.windows.net/bucket/data-train.arrow", 33 | ) 34 | print(codebox.list_files()) 35 | 36 | codebox.exec("import os") 37 | print(codebox.exec("print(os.listdir())")) 38 | print(codebox.exec("print([(f, os.path.getsize(f)) for f in os.listdir('.')])")) 39 | -------------------------------------------------------------------------------- /examples/custom_factory.py.todo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shroominic/codebox-api/1af3d800373991e8b6dae123f8673973b2ce8b45/examples/custom_factory.py.todo -------------------------------------------------------------------------------- /examples/docker_parallel_execution.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import time 3 | from pathlib import Path 4 | 5 | from codeboxapi import CodeBox 6 | 7 | 8 | async def train_model(codebox: CodeBox, data_split: int) -> dict: 9 | """Train a model on a subset of data.""" 10 | 11 | file = Path("examples/assets/advertising.csv") 12 | assert file.exists(), "Dataset file does not exist" 13 | 14 | # Upload dataset 15 | await codebox.aupload(file.name, file.read_bytes()) 16 | 17 | # Install required packages 18 | await codebox.ainstall("pandas") 19 | await codebox.ainstall("scikit-learn") 20 | 21 | # Training code with different data splits 22 | code = f""" 23 | import pandas as pd 24 | from sklearn.model_selection import train_test_split 25 | from sklearn.linear_model import LinearRegression 26 | from sklearn.metrics import mean_squared_error, r2_score 27 | 28 | # Load and prepare data 29 | data = pd.read_csv('advertising.csv') 30 | X = data[['TV', 'Radio', 'Newspaper']] 31 | y = data['Sales'] 32 | 33 | # Split with different random states for different data subsets 34 | X_train, X_test, y_train, y_test = train_test_split( 35 | X, y, test_size=0.3, random_state={data_split} 36 | ) 37 | 38 | # Train model 39 | model = LinearRegression() 40 | model.fit(X_train, y_train) 41 | 42 | # Evaluate 43 | y_pred = model.predict(X_test) 44 | mse = mean_squared_error(y_test, y_pred) 45 | r2 = r2_score(y_test, y_pred) 46 | 47 | print(f"Split {data_split}:") 48 | print(f"MSE: {{mse:.4f}}") 49 | print(f"R2: {{r2:.4f}}") 50 | print(f"Coefficients: {{model.coef_.tolist()}}") 51 | """ 52 | result = await codebox.aexec(code) 53 | return {"split": data_split, "output": result.text, "errors": result.errors} 54 | 55 | 56 | async def main(): 57 | # Create multiple Docker instances 58 | num_parallel = 4 59 | codeboxes = [CodeBox(api_key="docker") for _ in range(num_parallel)] 60 | 61 | # Create tasks for different data splits 62 | tasks = [] 63 | for i, codebox in enumerate(codeboxes): 64 | task = asyncio.create_task(train_model(codebox, i)) 65 | tasks.append(task) 66 | 67 | # Execute and time the parallel processing 68 | start_time = time.perf_counter() 69 | results = await asyncio.gather(*tasks) 70 | end_time = time.perf_counter() 71 | 72 | # Print results 73 | print(f"\nParallel execution completed in {end_time - start_time:.2f} seconds\n") 74 | for result in results: 75 | if not result["errors"]: 76 | print(f"Results for {result['split']}:") 77 | print(result["output"]) 78 | print("-" * 50) 79 | else: 80 | print(f"Error in split {result['split']}:", result["errors"]) 81 | 82 | 83 | if __name__ == "__main__": 84 | asyncio.run(main()) 85 | -------------------------------------------------------------------------------- /examples/file_conversion.py: -------------------------------------------------------------------------------- 1 | import httpx 2 | 3 | from codeboxapi import CodeBox 4 | 5 | codebox = CodeBox() 6 | 7 | # upload dataset csv 8 | csv_bytes = httpx.get( 9 | "https://archive.ics.uci.edu/ml/machine-learning-databases/iris/iris.data" 10 | ).content 11 | codebox.upload("iris.csv", csv_bytes) 12 | 13 | # install openpyxl for excel conversion 14 | codebox.install("pandas") 15 | codebox.install("openpyxl") 16 | 17 | # convert dataset csv to excel 18 | output = codebox.exec( 19 | "import pandas as pd\n\n" 20 | "df = pd.read_csv('iris.csv', header=None)\n\n" 21 | "df.to_excel('iris.xlsx', index=False)\n" 22 | "'iris.xlsx'" 23 | ) 24 | 25 | # check output type 26 | if output.images: 27 | print("This should not happen") 28 | elif output.errors: 29 | print("Error: ", output.errors) 30 | else: 31 | # all files inside the codebox 32 | for file in codebox.list_files(): 33 | print("File: ", file.path) 34 | print("File Size: ", file.get_size()) 35 | -------------------------------------------------------------------------------- /examples/getting_started.py: -------------------------------------------------------------------------------- 1 | from codeboxapi import CodeBox 2 | 3 | # Initialize CodeBox 4 | codebox = CodeBox(api_key="local") # or get your API key at https://codeboxapi.com 5 | 6 | # Basic Examples 7 | # ------------- 8 | 9 | # 1. Simple Code Execution 10 | result = codebox.exec("print('Hello World!')") 11 | print(result.text) # Output: Hello World! 12 | 13 | # 2. File Operations 14 | # Upload a file 15 | codebox.upload("example.txt", b"Hello from CodeBox!") 16 | 17 | # Download a file 18 | downloaded = codebox.download("example.txt") 19 | content = downloaded.get_content() # Returns b"Hello from CodeBox!" 20 | print("Content:\n", content, sep="") 21 | 22 | # List files 23 | files = codebox.list_files() 24 | print("\nFiles:\n", "\n".join(f.__repr__() for f in files), sep="") 25 | 26 | # 3. Package Management 27 | # Install packages 28 | codebox.install("pandas") 29 | 30 | # List installed packages 31 | packages = codebox.list_packages() 32 | print("\nFirst 10 packages:\n", "\n".join(packages[:10]), sep="") 33 | 34 | # 4. Variable Management 35 | # Execute code that creates variables 36 | codebox.exec(""" 37 | x = 42 38 | data = [1, 2, 3] 39 | name = "Alice" 40 | """) 41 | 42 | # Show all variables 43 | variables = codebox.show_variables() 44 | print("\nVariables:\n", "\n".join(f"{k}={v}" for k, v in variables.items()), sep="") 45 | 46 | # 5. Plotting with Matplotlib 47 | plot_code = """ 48 | import matplotlib.pyplot as plt 49 | plt.figure(figsize=(10, 5)) 50 | plt.plot([1, 2, 3, 4], [1, 4, 2, 3]) 51 | plt.title('My Plot') 52 | plt.show() 53 | """ 54 | result = codebox.exec(plot_code) 55 | # result.images will contain the plot as bytes 56 | 57 | # 6. Streaming Output 58 | # Useful for long-running operations 59 | for chunk in codebox.stream_exec(""" 60 | for i in range(5): 61 | print(f"Processing item {i}") 62 | import time 63 | time.sleep(1) 64 | """): 65 | # will not print when using "local" as api_key 66 | # due to stdout being captured in the background 67 | print(chunk.content, end="") 68 | 69 | # 7. Bash Commands 70 | # Execute bash commands 71 | codebox.exec("ls -la", kernel="bash") 72 | codebox.exec("pwd", kernel="bash") 73 | 74 | # Create and run Python scripts via bash 75 | codebox.exec("echo \"print('Running from file')\" > script.py", kernel="bash") 76 | codebox.exec("python script.py", kernel="bash") 77 | 78 | # 8. Error Handling 79 | result = codebox.exec("1/0") 80 | if result.errors: 81 | print("\nError occurred:", result.errors[0]) 82 | -------------------------------------------------------------------------------- /examples/langchain_agent.py.todo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shroominic/codebox-api/1af3d800373991e8b6dae123f8673973b2ce8b45/examples/langchain_agent.py.todo -------------------------------------------------------------------------------- /examples/local_docker.py.todo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shroominic/codebox-api/1af3d800373991e8b6dae123f8673973b2ce8b45/examples/local_docker.py.todo -------------------------------------------------------------------------------- /examples/plot_dataset.py: -------------------------------------------------------------------------------- 1 | import base64 2 | from io import BytesIO 3 | from pathlib import Path 4 | 5 | import httpx 6 | from PIL import Image 7 | 8 | from codeboxapi import CodeBox 9 | 10 | codebox = CodeBox(api_key="local") 11 | 12 | # download the iris dataset 13 | iris_csv_bytes = httpx.get( 14 | "https://archive.ics.uci.edu/ml/machine-learning-databases/iris/iris.data" 15 | ).content 16 | 17 | # upload the dataset to the codebox 18 | codebox.upload("iris.csv", iris_csv_bytes) 19 | 20 | # dataset analysis code 21 | file_path = Path("examples/assets/dataset_code.txt") 22 | 23 | # run the code 24 | output = codebox.exec(file_path) 25 | 26 | if output.images: 27 | img_bytes = base64.b64decode(output.images[0]) 28 | img_buffer = BytesIO(img_bytes) 29 | 30 | # Display the image 31 | img = Image.open(img_buffer) 32 | img.show() 33 | 34 | elif output.errors: 35 | print("Error:", output.errors) 36 | -------------------------------------------------------------------------------- /examples/simple_codeinterpreter.py.todo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shroominic/codebox-api/1af3d800373991e8b6dae123f8673973b2ce8b45/examples/simple_codeinterpreter.py.todo -------------------------------------------------------------------------------- /examples/stream_chunk_timing.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import time 3 | 4 | from codeboxapi import CodeBox, ExecChunk 5 | 6 | 7 | def sync_stream_exec(cb: CodeBox) -> None: 8 | chunks: list[tuple[ExecChunk, float]] = [] 9 | t0 = time.perf_counter() 10 | for chunk in cb.stream_exec( 11 | "import time;\nfor i in range(3): time.sleep(1); print(i)" 12 | ): 13 | chunks.append((chunk, time.perf_counter() - t0)) 14 | 15 | for chunk, t in chunks: 16 | print(f"{t:.5f}: {chunk}") 17 | 18 | 19 | async def async_stream_exec(cb: CodeBox) -> None: 20 | chunks: list[tuple[ExecChunk, float]] = [] 21 | t0 = time.perf_counter() 22 | async for chunk in cb.astream_exec( 23 | "import time;\nfor i in range(3): time.sleep(1); print(i)" 24 | ): 25 | chunks.append((chunk, time.perf_counter() - t0)) 26 | 27 | for chunk, t in chunks: 28 | print(f"{t:.5f}: {chunk}") 29 | 30 | 31 | print("remote") 32 | cb = CodeBox() 33 | sync_stream_exec(cb) 34 | asyncio.run(async_stream_exec(cb)) 35 | 36 | print("local") 37 | local = CodeBox(api_key="local") 38 | sync_stream_exec(local) 39 | asyncio.run(async_stream_exec(local)) 40 | 41 | print("docker") 42 | docker = CodeBox(api_key="docker") 43 | sync_stream_exec(docker) 44 | asyncio.run(async_stream_exec(docker)) 45 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: CodeBox API - Docs 2 | site_author: Shroominic 3 | site_url: https://shroominic.github.io/codebox-api/ 4 | repo_name: shroominic/codebox-api 5 | repo_url: https://github.com/shroominic/codebox-api/ 6 | 7 | nav: 8 | - 'Getting Started': 9 | - 'Welcome': 'index.md' 10 | - 'Installation': 'installation.md' 11 | - 'Usage': 'usage.md' 12 | - 'Settings': 'settings.md' 13 | - 'Concepts': 'concepts.md' 14 | - 'Examples': 'examples.md' 15 | - 'API Reference': 'api.md' 16 | 17 | theme: 18 | name: material 19 | palette: 20 | scheme: slate 21 | 22 | plugins: 23 | - neoteroi.mkdocsoad 24 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "codeboxapi" 3 | version = "0.2.6" 4 | description = "CodeBox gives you an easy scalable and isolated python interpreter for your LLM Agents." 5 | keywords = [ 6 | "codeboxapi", 7 | "code-interpreter", 8 | "codebox-api", 9 | "language model", 10 | "codeinterpreterapi", 11 | "code sandbox", 12 | "codebox", 13 | "agent infrastructure", 14 | "agents", 15 | "agent sandbox", 16 | ] 17 | authors = [{ name = "Shroominic", email = "contact@shroominic.com" }] 18 | dependencies = ["httpx>=0.27", "tenacity>=9.0.0"] 19 | readme = "README.md" 20 | requires-python = ">= 3.9" 21 | license = { text = "MIT" } 22 | 23 | [project.urls] 24 | repository = "https://github.com/shroominic/codebox-api" 25 | api-reference = "https://codeboxapi.com/docs" 26 | docs = "https://codeboxapi.com/docs" 27 | 28 | [build-system] 29 | requires = ["hatchling"] 30 | build-backend = "hatchling.build" 31 | 32 | [tool.rye] 33 | managed = true 34 | dev-dependencies = [ 35 | "codeboxapi[all]", 36 | "codeboxapi[docs]", 37 | "codeboxapi[pytest]", 38 | "codeboxapi[serve]", 39 | "ruff", 40 | "mypy", 41 | "types-aiofiles>=24", 42 | "types-pillow>=10", 43 | ] 44 | 45 | [project.optional-dependencies] 46 | docs = ["neoteroi-mkdocs", "mkdocs-material"] 47 | pytest = ["pytest-asyncio"] 48 | local = ["jupyter-client", "ipykernel", "uv", "aiofiles"] 49 | vision = ["Pillow"] 50 | serve = ["fastapi[standard]"] 51 | data-science = [ 52 | "codeboxapi[local]", 53 | "codeboxapi[vision]", 54 | "pandas", 55 | "numpy", 56 | "matplotlib", 57 | "seaborn", 58 | "scikit-learn", 59 | "uv", 60 | "bokeh", 61 | "dash", 62 | "matplotlib", 63 | "networkx", 64 | "numpy", 65 | "openpyxl", 66 | "pandas", 67 | "pillow", 68 | "plotly", 69 | "python-docx", 70 | "scikit-learn", 71 | "scipy", 72 | "seaborn", 73 | "statsmodels", 74 | "sympy", 75 | "yfinance", 76 | ] 77 | all = ["codeboxapi[data-science]", "codeboxapi[serve]"] 78 | 79 | [project.scripts] 80 | codeboxapi-serve = "codeboxapi.api:serve" 81 | 82 | [tool.hatch.metadata] 83 | allow-direct-references = true 84 | 85 | [tool.hatch.build.targets.wheel] 86 | packages = ["src/codeboxapi"] 87 | 88 | [tool.pytest.ini_options] 89 | filterwarnings = "ignore::DeprecationWarning" 90 | 91 | [tool.ruff.lint] 92 | select = ["E", "F", "I"] 93 | ignore = ["E701"] 94 | 95 | [tool.ruff.format] 96 | preview = true 97 | -------------------------------------------------------------------------------- /requirements.lock: -------------------------------------------------------------------------------- 1 | # generated by rye 2 | # use `rye lock` or `rye sync` to update this lockfile 3 | # 4 | # last locked with the following flags: 5 | # pre: false 6 | # features: [] 7 | # all-features: false 8 | # with-sources: false 9 | # generate-hashes: false 10 | # universal: false 11 | 12 | -e file:. 13 | anyio==4.8.0 14 | # via httpx 15 | certifi==2024.12.14 16 | # via httpcore 17 | # via httpx 18 | exceptiongroup==1.2.2 19 | # via anyio 20 | h11==0.14.0 21 | # via httpcore 22 | httpcore==1.0.7 23 | # via httpx 24 | httpx==0.28.1 25 | # via codeboxapi 26 | idna==3.10 27 | # via anyio 28 | # via httpx 29 | sniffio==1.3.1 30 | # via anyio 31 | tenacity==9.0.0 32 | # via codeboxapi 33 | typing-extensions==4.12.2 34 | # via anyio 35 | -------------------------------------------------------------------------------- /roadmap.todo: -------------------------------------------------------------------------------- 1 | [ ] - different python versions 2 | 3 | [ ] - gpu support 4 | 5 | [ ] - js kernel 6 | 7 | [ ] - metadata for kw storage 8 | 9 | [ ] - request only mode 10 | 11 | [ ] - s3 filesystems 12 | 13 | [ ] - add pypi requirements to factory 14 | 15 | [ ] - visual-chromium-box 16 | -------------------------------------------------------------------------------- /scripts/build.sh: -------------------------------------------------------------------------------- 1 | if [ -z "$1" ]; then 2 | echo "Error: No version supplied" 3 | echo "Usage: $0 " 4 | exit 1 5 | fi 6 | 7 | VERSION=$1 8 | 9 | docker build -t codebox . 10 | 11 | # todo move container to seperate codeboxapi account 12 | docker tag codebox shroominic/codebox:$VERSION 13 | docker tag codebox shroominic/codebox:latest 14 | 15 | docker push shroominic/codebox:$VERSION 16 | docker push shroominic/codebox:latest 17 | -------------------------------------------------------------------------------- /scripts/dev-setup.sh: -------------------------------------------------------------------------------- 1 | ruff lint . --fix 2 | 3 | # check if uv is installed or install it 4 | if ! command -v uv &> /dev/null 5 | then 6 | echo "uv not found, installing..." 7 | pip install uv 8 | else 9 | echo "uv is already installed" 10 | fi 11 | 12 | # check if venv exists or create it 13 | if [ ! -d ".venv" ]; then 14 | echo "Creating virtual environment..." 15 | uv venv 16 | else 17 | echo "Virtual environment already exists" 18 | fi 19 | 20 | # Install dependencies 21 | echo "Installing dependencies..." 22 | uv pip install -r pyproject.toml 23 | 24 | -------------------------------------------------------------------------------- /src/codeboxapi/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | CodeBox is the simplest cloud infrastructure for your LLM Apps and Services. 3 | 4 | The `codeboxapi` package provides a Python API Wrapper for the Codebox API. 5 | The package includes modules for configuring the client, setting the API key, 6 | and interacting with Codebox instances. 7 | """ 8 | 9 | from .codebox import CodeBox 10 | from .types import ExecChunk, ExecResult, RemoteFile 11 | 12 | __all__ = ["CodeBox", "ExecChunk", "ExecResult", "RemoteFile"] 13 | -------------------------------------------------------------------------------- /src/codeboxapi/api.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from contextlib import asynccontextmanager 3 | from datetime import datetime, timedelta 4 | from os import getenv, path 5 | import typing as t 6 | 7 | from fastapi import Body, Depends, FastAPI, HTTPException, UploadFile 8 | from fastapi.responses import FileResponse, StreamingResponse 9 | from pydantic import BaseModel 10 | 11 | from codeboxapi.utils import async_raise_timeout 12 | 13 | from .local import LocalBox 14 | 15 | codebox = LocalBox() 16 | last_interaction = datetime.utcnow() 17 | 18 | 19 | @asynccontextmanager 20 | async def lifespan(_: FastAPI) -> t.AsyncGenerator[None, None]: 21 | async def timeout(): 22 | if (_timeout := getenv("CODEBOX_TIMEOUT", "15")).lower() == "none": 23 | return 24 | while last_interaction + timedelta(minutes=float(_timeout)) > datetime.utcnow(): 25 | await asyncio.sleep(1) 26 | exit(0) 27 | 28 | t = asyncio.create_task(timeout()) 29 | yield 30 | t.cancel() 31 | 32 | 33 | async def get_codebox() -> t.AsyncGenerator[LocalBox, None]: 34 | global codebox, last_interaction 35 | last_interaction = datetime.utcnow() 36 | yield codebox 37 | 38 | 39 | app = FastAPI(title="Codebox API", lifespan=lifespan) 40 | app.get("/")(lambda: {"status": "ok"}) 41 | 42 | 43 | class ExecBody(BaseModel): 44 | code: str 45 | kernel: t.Literal["ipython", "bash"] = "ipython" 46 | timeout: t.Optional[int] = None 47 | cwd: t.Optional[str] = None 48 | 49 | 50 | @app.post("/exec") 51 | async def exec( 52 | exec: ExecBody, codebox: LocalBox = Depends(get_codebox) 53 | ) -> StreamingResponse: 54 | async def event_stream() -> t.AsyncGenerator[str, None]: 55 | async for chunk in codebox.astream_exec( 56 | exec.code, exec.kernel, exec.timeout, exec.cwd 57 | ): # protocol is content 58 | yield f"<{chunk.type}>{chunk.content}" 59 | 60 | return StreamingResponse(event_stream()) 61 | 62 | 63 | @app.get("/files/download/{file_name}") 64 | async def download( 65 | file_name: str, 66 | timeout: t.Optional[int] = None, 67 | codebox: LocalBox = Depends(get_codebox), 68 | ) -> FileResponse: 69 | async with async_raise_timeout(timeout): 70 | file_path = path.join(codebox.cwd, file_name) 71 | return FileResponse( 72 | path=file_path, media_type="application/octet-stream", filename=file_name 73 | ) 74 | 75 | 76 | @app.post("/files/upload") 77 | async def upload( 78 | file: UploadFile, 79 | timeout: t.Optional[int] = None, 80 | codebox: LocalBox = Depends(get_codebox), 81 | ) -> None: 82 | if not file.filename: 83 | raise HTTPException(status_code=400, detail="A file name is required") 84 | 85 | await codebox.aupload(file.filename, file.file, timeout) 86 | 87 | 88 | @app.post("/code/execute") 89 | async def deprecated_exec( 90 | body: dict = Body(), codebox: LocalBox = Depends(get_codebox) 91 | ) -> dict: 92 | """deprecated: use /exec instead""" 93 | ex = await codebox.aexec(body["properties"]["code"]) 94 | return {"properties": {"stdout": ex.text, "stderr": ex.errors, "result": ex.text}} 95 | 96 | 97 | def serve(): 98 | import uvicorn 99 | 100 | uvicorn.run(app, host="0.0.0.0", port=int(getenv("PORT", 8069))) 101 | 102 | 103 | if __name__ == "__main__": 104 | serve() 105 | -------------------------------------------------------------------------------- /src/codeboxapi/codebox.py: -------------------------------------------------------------------------------- 1 | """ 2 | CodeBox API 3 | ~~~~~~~~~~~ 4 | 5 | The main class for the CodeBox API. 6 | 7 | Usage 8 | ----- 9 | 10 | .. code-block:: python 11 | 12 | from codeboxapi import CodeBox 13 | 14 | codebox = CodeBox(api_key="local") 15 | 16 | codebox.healthcheck() 17 | codebox.exec("print('Hello World!')") 18 | codebox.upload("test.txt", "This is test file content!") 19 | codebox.exec("!pip install matplotlib", kernel="bash") 20 | codebox.list_files() 21 | codebox.download("test.txt") 22 | 23 | .. code-block:: python 24 | 25 | from codeboxapi import CodeBox 26 | 27 | codebox = CodeBox(api_key="local") 28 | 29 | await codebox.ahealthcheck() 30 | await codebox.aexec("print('Hello World!')") 31 | await codebox.ainstall("matplotlib") 32 | await codebox.aupload("test.txt", "This is test file content!") 33 | await codebox.alist_files() 34 | await codebox.adownload("test.txt") 35 | 36 | """ 37 | 38 | import os 39 | import typing as t 40 | from importlib import import_module 41 | 42 | import anyio 43 | 44 | from .utils import async_flatten_exec_result, deprecated, flatten_exec_result, syncify 45 | 46 | if t.TYPE_CHECKING: 47 | from .types import CodeBoxOutput, ExecChunk, ExecResult, RemoteFile 48 | 49 | 50 | class CodeBox: 51 | def __new__(cls, *args, **kwargs) -> "CodeBox": 52 | """ 53 | Creates a CodeBox session 54 | """ 55 | api_key = kwargs.get("api_key") or os.getenv("CODEBOX_API_KEY") 56 | # todo make sure "local" is not hardcoded default 57 | if api_key == "local": 58 | return import_module("codeboxapi.local").LocalBox(*args, **kwargs) 59 | 60 | if api_key == "docker": 61 | return import_module("codeboxapi.docker").DockerBox(*args, **kwargs) 62 | return import_module("codeboxapi.remote").RemoteBox(*args, **kwargs) 63 | 64 | def __init__( 65 | self, 66 | session_id: t.Optional[str] = None, 67 | api_key: t.Optional[t.Union[str, t.Literal["local", "docker"]]] = None, 68 | factory_id: t.Optional[t.Union[str, t.Literal["default"]]] = None, 69 | **_: bool, 70 | ) -> None: 71 | self.session_id = session_id or "local" 72 | self.api_key = api_key or os.getenv("CODEBOX_API_KEY", "local") 73 | self.factory_id = factory_id or os.getenv("CODEBOX_FACTORY_ID", "default") 74 | 75 | # SYNC 76 | 77 | def exec( 78 | self, 79 | code: t.Union[str, os.PathLike], 80 | kernel: t.Literal["ipython", "bash"] = "ipython", 81 | timeout: t.Optional[float] = None, 82 | cwd: t.Optional[str] = None, 83 | ) -> "ExecResult": 84 | """Execute code inside the CodeBox instance""" 85 | return flatten_exec_result(self.stream_exec(code, kernel, timeout, cwd)) 86 | 87 | def stream_exec( 88 | self, 89 | code: t.Union[str, os.PathLike], 90 | kernel: t.Literal["ipython", "bash"] = "ipython", 91 | timeout: t.Optional[float] = None, 92 | cwd: t.Optional[str] = None, 93 | ) -> t.Generator["ExecChunk", None, None]: 94 | """Executes the code and streams the result.""" 95 | raise NotImplementedError("Abstract method, please use a subclass.") 96 | 97 | def upload( 98 | self, 99 | remote_file_path: str, 100 | content: t.Union[t.BinaryIO, bytes, str], 101 | timeout: t.Optional[float] = None, 102 | ) -> "RemoteFile": 103 | """Upload a file to the CodeBox instance""" 104 | raise NotImplementedError("Abstract method, please use a subclass.") 105 | 106 | def stream_download( 107 | self, 108 | remote_file_path: str, 109 | timeout: t.Optional[float] = None, 110 | ) -> t.Generator[bytes, None, None]: 111 | """Download a file as open BinaryIO. Make sure to close the file after use.""" 112 | raise NotImplementedError("Abstract method, please use a subclass.") 113 | 114 | # ASYNC 115 | 116 | async def aexec( 117 | self, 118 | code: t.Union[str, os.PathLike], 119 | kernel: t.Literal["ipython", "bash"] = "ipython", 120 | timeout: t.Optional[float] = None, 121 | cwd: t.Optional[str] = None, 122 | ) -> "ExecResult": 123 | """Async Execute python code inside the CodeBox instance""" 124 | return await async_flatten_exec_result( 125 | self.astream_exec(code, kernel, timeout, cwd) 126 | ) 127 | 128 | def astream_exec( 129 | self, 130 | code: t.Union[str, os.PathLike], 131 | kernel: t.Literal["ipython", "bash"] = "ipython", 132 | timeout: t.Optional[float] = None, 133 | cwd: t.Optional[str] = None, 134 | ) -> t.AsyncGenerator["ExecChunk", None]: 135 | """Async Stream Chunks of Execute python code inside the CodeBox instance""" 136 | raise NotImplementedError("Abstract method, please use a subclass.") 137 | 138 | async def aupload( 139 | self, 140 | remote_file_path: str, 141 | content: t.Union[t.BinaryIO, bytes, str], 142 | timeout: t.Optional[float] = None, 143 | ) -> "RemoteFile": 144 | """Async Upload a file to the CodeBox instance""" 145 | raise NotImplementedError("Abstract method, please use a subclass.") 146 | 147 | async def adownload( 148 | self, 149 | remote_file_path: str, 150 | timeout: t.Optional[float] = None, 151 | ) -> "RemoteFile": 152 | return [f for f in (await self.alist_files()) if f.path in remote_file_path][0] 153 | 154 | def astream_download( 155 | self, 156 | remote_file_path: str, 157 | timeout: t.Optional[float] = None, 158 | ) -> t.AsyncGenerator[bytes, None]: 159 | """Async Download a file as BinaryIO. Make sure to close the file after use.""" 160 | raise NotImplementedError("Abstract method, please use a subclass.") 161 | 162 | # HELPER METHODS 163 | 164 | async def ahealthcheck(self) -> t.Literal["healthy", "error"]: 165 | return ( 166 | "healthy" 167 | if "ok" in (await self.aexec("echo ok", kernel="bash")).text 168 | else "error" 169 | ) 170 | 171 | async def ainstall(self, *packages: str) -> str: 172 | # todo make sure it always uses the correct python venv 173 | await self.aexec( 174 | "uv pip install " + " ".join(packages), 175 | kernel="bash", 176 | ) 177 | return " ".join(packages) + " installed successfully" 178 | 179 | async def afile_from_url(self, url: str, file_path: str) -> "RemoteFile": 180 | """ 181 | Download a file from a URL to the specified destination in the CodeBox. 182 | Example: 183 | >>> codebox.afile_from_url("https://github.com/org/repo/file.txt", "file.txt") 184 | """ 185 | code = ( 186 | "import httpx\n" 187 | "async with httpx.AsyncClient() as client:\n" 188 | f" async with client.stream('GET', '{url}') as response:\n" 189 | " response.raise_for_status()\n" 190 | f" with open('{file_path}', 'wb') as f:\n" 191 | " async for chunk in response.aiter_bytes():\n" 192 | " f.write(chunk)\n" 193 | ) 194 | await self.aexec(code) 195 | return await self.adownload(file_path) 196 | 197 | async def alist_files(self) -> list["RemoteFile"]: 198 | from .types import RemoteFile 199 | 200 | files = ( 201 | await self.aexec( 202 | "find . -type f -exec du -h {} + | awk '{print $2, $1}' | sort", 203 | kernel="bash", 204 | ) 205 | ).text.splitlines() 206 | return [ 207 | RemoteFile( 208 | path=parts[0].removeprefix("./"), 209 | remote=self, 210 | _size=self._parse_size(parts[1]), 211 | ) 212 | for file in files 213 | if (parts := file.split(" ")) and len(parts) == 2 214 | ] 215 | 216 | def _parse_size(self, size_str: str) -> int: 217 | """Convert human-readable size to bytes.""" 218 | units = {"K": 1024, "M": 1024**2, "G": 1024**3, "T": 1024**4} 219 | try: 220 | number = float(size_str[:-1]) 221 | unit = size_str[-1].upper() 222 | return int(number * units.get(unit, 1)) 223 | except ValueError: 224 | return -1 225 | 226 | async def alist_packages(self) -> list[str]: 227 | return ( 228 | await self.aexec( 229 | "uv pip list | tail -n +3 | cut -d ' ' -f 1", 230 | kernel="bash", 231 | ) 232 | ).text.splitlines() 233 | 234 | async def ashow_variables(self) -> dict[str, str]: 235 | vars = [ 236 | line.strip() for line in (await self.aexec("%who")).text.strip().split() 237 | ] 238 | return {v: (await self.aexec(f"print({v}, end='')")).text for v in vars} 239 | 240 | async def arestart(self) -> None: 241 | """Restart the Jupyter kernel""" 242 | await self.aexec(r"%restart") 243 | 244 | async def akeep_alive(self, minutes: int = 15) -> None: 245 | """Keep the CodeBox instance alive for a certain amount of minutes""" 246 | 247 | async def ping(cb: CodeBox, d: int) -> None: 248 | for _ in range(d): 249 | await cb.ahealthcheck() 250 | await anyio.sleep(60) 251 | 252 | async with anyio.create_task_group() as tg: 253 | tg.start_soon(ping, self, minutes) 254 | 255 | # SYNCIFY 256 | 257 | def download( 258 | self, remote_file_path: str, timeout: t.Optional[float] = None 259 | ) -> "RemoteFile": 260 | return syncify(self.adownload)(remote_file_path, timeout) 261 | 262 | def healthcheck(self) -> str: 263 | return syncify(self.ahealthcheck)() 264 | 265 | def install(self, *packages: str) -> str: 266 | return syncify(self.ainstall)(*packages) 267 | 268 | def file_from_url(self, url: str, file_path: str) -> "RemoteFile": 269 | return syncify(self.afile_from_url)(url, file_path) 270 | 271 | def list_files(self) -> list["RemoteFile"]: 272 | return syncify(self.alist_files)() 273 | 274 | def list_packages(self) -> list[str]: 275 | return syncify(self.alist_packages)() 276 | 277 | def show_variables(self) -> dict[str, str]: 278 | return syncify(self.ashow_variables)() 279 | 280 | def restart(self) -> None: 281 | return syncify(self.arestart)() 282 | 283 | def keep_alive(self, minutes: int = 15) -> None: 284 | return syncify(self.akeep_alive)(minutes) 285 | 286 | # DEPRECATED 287 | 288 | @deprecated( 289 | "There is no need anymore to explicitly start a CodeBox instance.\n" 290 | "When calling any method you will get assigned a new session.\n" 291 | "The `.start` method is deprecated. Use `.healthcheck` instead." 292 | ) 293 | async def astart(self) -> t.Literal["started", "error"]: 294 | return "started" if (await self.ahealthcheck()) == "healthy" else "error" 295 | 296 | @deprecated( 297 | "The `.stop` method is deprecated. " 298 | "The session will be closed automatically after the last interaction.\n" 299 | "(default timeout: 15 minutes)" 300 | ) 301 | async def astop(self) -> t.Literal["stopped"]: 302 | return "stopped" 303 | 304 | @deprecated( 305 | "The `.run` method is deprecated. Use `.exec` instead.", 306 | ) 307 | async def arun(self, code: t.Union[str, os.PathLike]) -> "CodeBoxOutput": 308 | from .types import CodeBoxOutput 309 | 310 | exec_result = await self.aexec(code, kernel="ipython") 311 | if exec_result.images: 312 | return CodeBoxOutput(type="image/png", content=exec_result.images[0]) 313 | if exec_result.errors: 314 | return CodeBoxOutput(type="stderr", content=exec_result.errors[0]) 315 | return CodeBoxOutput(type="stdout", content=exec_result.text) 316 | 317 | @deprecated( 318 | "The `.status` method is deprecated. Use `.healthcheck` instead.", 319 | ) 320 | async def astatus(self) -> t.Literal["started", "running", "stopped"]: 321 | return "running" if await self.ahealthcheck() == "healthy" else "stopped" 322 | 323 | @deprecated( 324 | "The `.start` method is deprecated. Use `.healthcheck` instead.", 325 | ) 326 | def start(self) -> t.Literal["started", "error"]: 327 | return syncify(self.astart)() 328 | 329 | @deprecated( 330 | "The `.stop` method is deprecated. " 331 | "The session will be closed automatically after the last interaction.\n" 332 | "(default timeout: 15 minutes)" 333 | ) 334 | def stop(self) -> t.Literal["stopped"]: 335 | return syncify(self.astop)() 336 | 337 | @deprecated( 338 | "The `.run` method is deprecated. Use `.exec` instead.", 339 | ) 340 | def run(self, code: t.Union[str, os.PathLike]) -> "CodeBoxOutput": 341 | return syncify(self.arun)(code) 342 | 343 | @deprecated( 344 | "The `.status` method is deprecated. Use `.healthcheck` instead.", 345 | ) 346 | def status(self) -> t.Literal["started", "running", "stopped"]: 347 | return syncify(self.astatus)() 348 | -------------------------------------------------------------------------------- /src/codeboxapi/docker.py: -------------------------------------------------------------------------------- 1 | import socket 2 | import subprocess 3 | import time 4 | import typing as t 5 | 6 | import httpx 7 | 8 | from .remote import RemoteBox 9 | 10 | 11 | def get_free_port(port_or_range: t.Union[int, t.Tuple[int, int]]) -> int: 12 | if isinstance(port_or_range, int): 13 | port = port_or_range 14 | else: 15 | start, end = port_or_range 16 | port = start 17 | 18 | while port <= (end if isinstance(port_or_range, tuple) else port): 19 | with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: 20 | try: 21 | s.bind(("localhost", port)) 22 | s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 23 | return port 24 | except OSError: 25 | port += 1 26 | 27 | raise OSError("No free ports available on the specified port or range.") 28 | 29 | 30 | class DockerBox(RemoteBox): 31 | def __init__( 32 | self, 33 | port_or_range: t.Union[int, t.Tuple[int, int]] = 8069, 34 | image: str = "shroominic/codebox:latest", 35 | timeout: float = 3, # minutes 36 | start_container: bool = True, 37 | **_, 38 | ) -> None: 39 | if start_container: 40 | self.port = get_free_port(port_or_range) 41 | subprocess.run( 42 | [ 43 | "docker", 44 | "run", 45 | "-d", 46 | "--rm", 47 | "-e", 48 | f"CODEBOX_TIMEOUT={timeout}", 49 | "-p", 50 | f"{self.port}:8069", 51 | image, 52 | ], 53 | check=True, 54 | stdout=subprocess.DEVNULL, 55 | stderr=subprocess.DEVNULL, 56 | ) 57 | else: 58 | assert isinstance(port_or_range, int) 59 | self.port = port_or_range 60 | self.session_id = str(self.port) 61 | self.base_url = f"http://localhost:{self.port}" 62 | self.client = httpx.Client(base_url=self.base_url) 63 | self.aclient = httpx.AsyncClient(base_url=self.base_url) 64 | self.api_key = "docker" 65 | self.factory_id = image 66 | self.session_id = str(self.port) 67 | self._wait_for_startup() 68 | 69 | def _wait_for_startup(self) -> None: 70 | while True: 71 | try: 72 | self.client.get("/") 73 | break 74 | except httpx.HTTPError: 75 | time.sleep(1) 76 | -------------------------------------------------------------------------------- /src/codeboxapi/local.py: -------------------------------------------------------------------------------- 1 | """ 2 | Local implementation of CodeBox. 3 | This is useful for testing and development. 4 | In case you don't put an api_key, 5 | this is the default CodeBox. 6 | """ 7 | 8 | import asyncio 9 | import base64 10 | import io 11 | import os 12 | import re 13 | import subprocess 14 | import sys 15 | import tempfile as tmpf 16 | import threading 17 | import time 18 | import typing as t 19 | from builtins import print as important_print 20 | from queue import Queue 21 | 22 | from IPython.core.interactiveshell import ExecutionResult, InteractiveShell 23 | 24 | from .codebox import CodeBox 25 | from .types import ExecChunk, RemoteFile 26 | from .utils import ( 27 | async_raise_timeout, 28 | check_installed, 29 | raise_timeout, 30 | resolve_pathlike, 31 | run_inside, 32 | ) 33 | 34 | IMAGE_PATTERN = r"(.*?)" 35 | 36 | 37 | class LocalBox(CodeBox): 38 | """ 39 | LocalBox is a CodeBox implementation that runs code locally using IPython. 40 | This is useful for testing and development. 41 | 42 | Only one instance can exist at a time. For parallel execution, use: 43 | - DockerBox for local parallel execution 44 | - Get an API key at codeboxapi.com for hosted parallel execution 45 | """ 46 | 47 | _instance: t.Optional["LocalBox"] = None 48 | 49 | def __new__(cls, *args, **kwargs) -> "LocalBox": 50 | if cls._instance: 51 | raise RuntimeError( 52 | "Only one LocalBox instance can exist at a time.\n" 53 | "For parallel execution use:\n" 54 | "- Use DockerBox for local parallel execution\n" 55 | "- Get an API key at https://codeboxapi.com for secure remote execution" 56 | ) 57 | 58 | # This is a hack to ignore the CodeBox.__new__ factory method 59 | instance = object.__new__(cls) 60 | cls._instance = instance 61 | return instance 62 | 63 | def __init__( 64 | self, 65 | session_id: t.Optional[str] = None, 66 | codebox_cwd: str = ".codebox", 67 | **kwargs, 68 | ) -> None: 69 | self.api_key = "local" 70 | self.factory_id = "local" 71 | self.session_id = session_id or "" 72 | os.makedirs(codebox_cwd, exist_ok=True) 73 | self.cwd = os.path.abspath(codebox_cwd) 74 | check_installed("ipython") 75 | self.shell = InteractiveShell.instance() 76 | self.shell.enable_gui = lambda x: None # type: ignore 77 | self._patch_matplotlib_show() 78 | 79 | def _patch_matplotlib_show(self) -> None: 80 | import matplotlib 81 | 82 | matplotlib.use("Agg") 83 | import matplotlib.pyplot as plt 84 | 85 | def custom_show(close=True): 86 | fig = plt.gcf() 87 | buf = io.BytesIO() 88 | fig.savefig(buf, format="png") 89 | buf.seek(0) 90 | img_str = base64.b64encode(buf.getvalue()).decode("utf-8") 91 | important_print(IMAGE_PATTERN.replace("(.*?)", img_str)) 92 | if close: 93 | plt.close(fig) 94 | 95 | plt.show = custom_show 96 | 97 | def stream_exec( 98 | self, 99 | code: t.Union[str, os.PathLike], 100 | kernel: t.Literal["ipython", "bash"] = "ipython", 101 | timeout: t.Optional[float] = None, 102 | cwd: t.Optional[str] = None, 103 | ) -> t.Generator[ExecChunk, None, None]: 104 | with raise_timeout(timeout): 105 | code = resolve_pathlike(code) 106 | 107 | if kernel == "ipython": 108 | with run_inside(cwd or self.cwd): 109 | old_stdout, old_stderr = sys.stdout, sys.stderr 110 | temp_output, temp_error = sys.stdout, sys.stderr = ( 111 | io.StringIO(), 112 | io.StringIO(), 113 | ) 114 | queue: Queue[t.Optional[ExecChunk]] = Queue() 115 | _result: list[ExecutionResult] = [] 116 | 117 | def _run_cell(c: str, result: list[ExecutionResult]) -> None: 118 | time.sleep(0.001) 119 | result.append(self.shell.run_cell(c)) 120 | 121 | run_cell = threading.Thread(target=_run_cell, args=(code, _result)) 122 | try: 123 | 124 | def stream_chunks(_out: io.StringIO, _err: io.StringIO) -> None: 125 | while run_cell.is_alive(): 126 | time.sleep(0.001) 127 | if output := _out.getvalue(): 128 | # todo make this more efficient? 129 | sys.stdout = _out = io.StringIO() 130 | 131 | if "" in output: 132 | image_matches = re.findall( 133 | IMAGE_PATTERN, output 134 | ) 135 | for img_str in image_matches: 136 | queue.put( 137 | ExecChunk(type="img", content=img_str) 138 | ) 139 | output = re.sub(IMAGE_PATTERN, "", output) 140 | 141 | if output: 142 | if output.startswith("Out["): 143 | # todo better disable logging somehow 144 | output = re.sub( 145 | r"Out\[(.*?)\]: ", "", output.strip() 146 | ) 147 | queue.put(ExecChunk(type="txt", content=output)) 148 | 149 | if error := _err.getvalue(): 150 | # todo make this more efficient? 151 | sys.stderr = _err = io.StringIO() 152 | queue.put(ExecChunk(type="err", content=error)) 153 | 154 | queue.put(None) 155 | 156 | stream = threading.Thread( 157 | target=stream_chunks, args=(temp_output, temp_error) 158 | ) 159 | 160 | run_cell.start() 161 | stream.start() 162 | 163 | while True: 164 | time.sleep(0.001) 165 | if queue.qsize() > 0: 166 | if chunk := queue.get(): 167 | yield chunk 168 | else: 169 | break 170 | 171 | result = _result[0] 172 | if result.error_before_exec: 173 | yield ExecChunk( 174 | type="err", 175 | content=str(result.error_before_exec).replace( 176 | "\\n", "\n" 177 | ), 178 | ) 179 | elif result.error_in_exec: 180 | yield ExecChunk( 181 | type="err", 182 | content=str(result.error_in_exec).replace("\\n", "\n"), 183 | ) 184 | elif result.result is not None: 185 | yield ExecChunk(type="txt", content=str(result.result)) 186 | 187 | finally: 188 | sys.stdout = old_stdout 189 | sys.stderr = old_stderr 190 | run_cell._stop() # type: ignore 191 | 192 | elif kernel == "bash": 193 | # todo maybe implement using queue 194 | process = subprocess.Popen( 195 | code, 196 | cwd=cwd or self.cwd, 197 | shell=True, 198 | stdout=subprocess.PIPE, 199 | stderr=subprocess.PIPE, 200 | ) 201 | if process.stdout: 202 | for c in process.stdout: 203 | yield ExecChunk(content=c.decode(), type="txt") 204 | if process.stderr: 205 | for c in process.stderr: 206 | yield ExecChunk(content=c.decode(), type="err") 207 | 208 | else: 209 | raise ValueError(f"Unsupported kernel: {kernel}") 210 | 211 | async def astream_exec( 212 | self, 213 | code: t.Union[str, os.PathLike], 214 | kernel: t.Literal["ipython", "bash"] = "ipython", 215 | timeout: t.Optional[float] = None, 216 | cwd: t.Optional[str] = None, 217 | ) -> t.AsyncGenerator[ExecChunk, None]: 218 | async with async_raise_timeout(timeout): 219 | code = resolve_pathlike(code) 220 | 221 | if kernel == "ipython": 222 | with run_inside(cwd or self.cwd): 223 | old_stdout, old_stderr = sys.stdout, sys.stderr 224 | temp_output, temp_error = sys.stdout, sys.stderr = ( 225 | io.StringIO(), 226 | io.StringIO(), 227 | ) 228 | 229 | run_cell = asyncio.create_task( 230 | asyncio.to_thread(self.shell.run_cell, code) 231 | ) 232 | try: 233 | while not run_cell.done(): 234 | await asyncio.sleep(0.001) 235 | if output := temp_output.getvalue(): 236 | # todo make this more efficient? 237 | sys.stdout = temp_output = io.StringIO() 238 | 239 | if "" in output: 240 | image_matches = re.findall(IMAGE_PATTERN, output) 241 | for img_str in image_matches: 242 | yield ExecChunk(type="img", content=img_str) 243 | output = re.sub(IMAGE_PATTERN, "", output) 244 | 245 | if output: 246 | if output.startswith("Out["): 247 | # todo better disable logging somehow 248 | output = re.sub( 249 | r"Out\[(.*?)\]: ", "", output.strip() 250 | ) 251 | yield ExecChunk(type="txt", content=output) 252 | 253 | if error := temp_error.getvalue(): 254 | sys.stderr = temp_error = io.StringIO() 255 | yield ExecChunk(type="err", content=error) 256 | 257 | result = await run_cell 258 | if result.error_before_exec: 259 | yield ExecChunk( 260 | type="err", content=str(result.error_before_exec) 261 | ) 262 | elif result.error_in_exec: 263 | yield ExecChunk( 264 | type="err", content=str(result.error_in_exec) 265 | ) 266 | elif result.result is not None: 267 | yield ExecChunk(type="txt", content=str(result.result)) 268 | finally: 269 | sys.stdout = old_stdout 270 | sys.stderr = old_stderr 271 | run_cell.cancel() 272 | 273 | elif kernel == "bash": 274 | process = await asyncio.create_subprocess_shell( 275 | code, 276 | cwd=cwd or self.cwd, 277 | stdout=asyncio.subprocess.PIPE, 278 | stderr=asyncio.subprocess.PIPE, 279 | ) 280 | 281 | # todo yield at the same time and not after each other 282 | if process.stdout: 283 | async for chunk in process.stdout: 284 | yield ExecChunk(content=chunk.decode(), type="txt") 285 | 286 | if process.stderr: 287 | async for err in process.stderr: 288 | yield ExecChunk(content=err.decode(), type="err") 289 | else: 290 | raise ValueError(f"Unsupported kernel: {kernel}") 291 | 292 | def upload( 293 | self, 294 | remote_file_path: str, 295 | content: t.Union[t.BinaryIO, bytes, str], 296 | timeout: t.Optional[float] = None, 297 | ) -> "RemoteFile": 298 | from .types import RemoteFile 299 | 300 | with raise_timeout(timeout): 301 | file_path = os.path.join(self.cwd, remote_file_path) 302 | with open(file_path, "wb") as file: 303 | if isinstance(content, str): 304 | file.write(content.encode()) 305 | elif isinstance(content, bytes): 306 | file.write(content) 307 | elif isinstance(content, tmpf.SpooledTemporaryFile): 308 | file.write(content.read()) 309 | elif isinstance(content, (t.BinaryIO, io.BytesIO)): 310 | file.write(content.read()) 311 | else: 312 | raise TypeError("Unsupported content type") 313 | return RemoteFile(path=remote_file_path, remote=self) 314 | 315 | async def aupload( 316 | self, 317 | file_name: str, 318 | content: t.Union[t.BinaryIO, bytes, str, tmpf.SpooledTemporaryFile], 319 | timeout: t.Optional[float] = None, 320 | ) -> RemoteFile: 321 | import aiofiles.os 322 | 323 | from .types import RemoteFile 324 | 325 | async with async_raise_timeout(timeout): 326 | file_path = os.path.join(self.cwd, file_name) 327 | async with aiofiles.open(file_path, "wb") as file: 328 | if isinstance(content, str): 329 | await file.write(content.encode()) 330 | elif isinstance(content, tmpf.SpooledTemporaryFile): 331 | await file.write(content.read()) 332 | elif isinstance(content, (t.BinaryIO, io.BytesIO)): 333 | try: 334 | while chunk := content.read(8192): 335 | await file.write(chunk) 336 | except ValueError as e: 337 | if "I/O operation on closed file" in str(e): 338 | # If the file is closed, we can't reopen it 339 | # Instead, we'll raise a more informative error 340 | raise ValueError( 341 | "The provided file object is closed and cannot be read" 342 | ) from e 343 | else: 344 | raise 345 | elif isinstance(content, bytes): 346 | await file.write(content) 347 | else: 348 | print(type(content), content.__dict__) 349 | raise TypeError("Unsupported content type") 350 | return RemoteFile(path=file_path, remote=self) 351 | 352 | def stream_download( 353 | self, 354 | remote_file_path: str, 355 | timeout: t.Optional[float] = None, 356 | ) -> t.Generator[bytes, None, None]: 357 | with raise_timeout(timeout): 358 | with open(os.path.join(self.cwd, remote_file_path), "rb") as f: 359 | while chunk := f.read(8192): 360 | yield chunk 361 | 362 | async def astream_download( 363 | self, 364 | remote_file_path: str, 365 | timeout: t.Optional[float] = None, 366 | ) -> t.AsyncGenerator[bytes, None]: 367 | import aiofiles 368 | 369 | async with async_raise_timeout(timeout): 370 | async with aiofiles.open( 371 | os.path.join(self.cwd, remote_file_path), "rb" 372 | ) as f: 373 | while chunk := await f.read(8192): 374 | yield chunk 375 | -------------------------------------------------------------------------------- /src/codeboxapi/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shroominic/codebox-api/1af3d800373991e8b6dae123f8673973b2ce8b45/src/codeboxapi/py.typed -------------------------------------------------------------------------------- /src/codeboxapi/remote.py: -------------------------------------------------------------------------------- 1 | import re 2 | import typing as t 3 | from os import PathLike, getenv 4 | from uuid import uuid4 5 | 6 | import anyio 7 | import httpx 8 | from tenacity import ( 9 | retry, 10 | retry_if_exception, 11 | stop_after_attempt, 12 | wait_exponential, 13 | ) 14 | 15 | from .codebox import CodeBox 16 | from .types import ExecChunk, RemoteFile 17 | from .utils import raise_error, resolve_pathlike 18 | 19 | 20 | class RemoteBox(CodeBox): 21 | """ 22 | Sandboxed Python Interpreter 23 | """ 24 | 25 | def __new__(cls, *args, **kwargs) -> "RemoteBox": 26 | # This is a hack to ignore the CodeBox.__new__ factory method. 27 | return object.__new__(cls) 28 | 29 | def __init__( 30 | self, 31 | session_id: t.Optional[str] = None, 32 | api_key: t.Optional[t.Union[str, t.Literal["local", "docker"]]] = None, 33 | factory_id: t.Optional[t.Union[str, t.Literal["default"]]] = None, 34 | base_url: t.Optional[str] = None, 35 | ) -> None: 36 | self.session_id = session_id or uuid4().hex 37 | self.factory_id = factory_id or getenv("CODEBOX_FACTORY_ID", "default") 38 | assert self.factory_id is not None 39 | self.api_key = ( 40 | api_key 41 | or getenv("CODEBOX_API_KEY") 42 | or raise_error("CODEBOX_API_KEY is required") 43 | ) 44 | self.base_url = base_url or getenv( 45 | "CODEBOX_BASE_URL", "https://codeboxapi.com/api/v2" 46 | ) 47 | self.url = f"{self.base_url}/codebox/{self.session_id}" 48 | self.headers = { 49 | "Factory-Id": self.factory_id, 50 | "Authorization": f"Bearer {self.api_key}", 51 | } 52 | self.client = httpx.Client(base_url=self.url, headers=self.headers) 53 | self.aclient = httpx.AsyncClient(base_url=self.url, headers=self.headers) 54 | 55 | @retry( 56 | retry=retry_if_exception( 57 | lambda e: isinstance(e, httpx.HTTPStatusError) 58 | and e.response.status_code == 502 59 | ), 60 | wait=wait_exponential(multiplier=1, min=5, max=150), 61 | stop=stop_after_attempt(3), 62 | ) 63 | def stream_exec( 64 | self, 65 | code: t.Union[str, PathLike], 66 | kernel: t.Literal["ipython", "bash"] = "ipython", 67 | timeout: t.Optional[float] = None, 68 | cwd: t.Optional[str] = None, 69 | ) -> t.Generator[ExecChunk, None, None]: 70 | code = resolve_pathlike(code) 71 | with self.client.stream( 72 | method="POST", 73 | url="/exec", 74 | timeout=timeout, 75 | json={"code": code, "kernel": kernel, "cwd": cwd}, 76 | ) as response: 77 | response.raise_for_status() 78 | buffer = "" 79 | for chunk in response.iter_text(): 80 | buffer += chunk 81 | while match := re.match( 82 | r"<(txt|img|err)>(.*?)", buffer, re.DOTALL 83 | ): 84 | _, end = match.span() 85 | t, c = match.groups() 86 | yield ExecChunk(type=t, content=c) # type: ignore[arg-type] 87 | buffer = buffer[end:] 88 | 89 | @retry( 90 | retry=retry_if_exception( 91 | lambda e: isinstance(e, httpx.HTTPStatusError) 92 | and e.response.status_code == 502 93 | ), 94 | wait=wait_exponential(multiplier=1, min=5, max=150), 95 | stop=stop_after_attempt(3), 96 | ) 97 | async def astream_exec( 98 | self, 99 | code: t.Union[str, PathLike], 100 | kernel: t.Literal["ipython", "bash"] = "ipython", 101 | timeout: t.Optional[float] = None, 102 | cwd: t.Optional[str] = None, 103 | ) -> t.AsyncGenerator[ExecChunk, None]: 104 | code = resolve_pathlike(code) 105 | try: 106 | async with self.aclient.stream( 107 | method="POST", 108 | url="/exec", 109 | timeout=timeout, 110 | json={"code": code, "kernel": kernel, "cwd": cwd}, 111 | ) as response: 112 | response.raise_for_status() 113 | buffer = "" 114 | async for chunk in response.aiter_text(): 115 | buffer += chunk 116 | while match := re.match( 117 | r"<(txt|img|err)>(.*?)", buffer, re.DOTALL 118 | ): 119 | _, end = match.span() 120 | t, c = match.groups() 121 | yield ExecChunk(type=t, content=c) # type: ignore[arg-type] 122 | buffer = buffer[end:] 123 | except RuntimeError as e: 124 | if "loop is closed" not in str(e): 125 | raise e 126 | await anyio.sleep(0.1) 127 | async for c in self.astream_exec(code, kernel, timeout, cwd): 128 | yield c 129 | 130 | @retry( 131 | retry=retry_if_exception( 132 | lambda e: isinstance(e, httpx.HTTPStatusError) 133 | and e.response.status_code == 502 134 | ), 135 | wait=wait_exponential(multiplier=1, min=5, max=150), 136 | stop=stop_after_attempt(3), 137 | ) 138 | def upload( 139 | self, 140 | file_name: str, 141 | content: t.Union[t.BinaryIO, bytes, str], 142 | timeout: t.Optional[float] = None, 143 | ) -> "RemoteFile": 144 | from .types import RemoteFile 145 | 146 | if isinstance(content, str): 147 | content = content.encode("utf-8") 148 | self.client.post( 149 | url="/files/upload", 150 | files={"file": (file_name, content)}, 151 | timeout=timeout, 152 | ).raise_for_status() 153 | return RemoteFile(path=file_name, remote=self) 154 | 155 | @retry( 156 | retry=retry_if_exception( 157 | lambda e: isinstance(e, httpx.HTTPStatusError) 158 | and e.response.status_code == 502 159 | ), 160 | wait=wait_exponential(multiplier=1, min=5, max=150), 161 | stop=stop_after_attempt(3), 162 | ) 163 | async def aupload( 164 | self, 165 | remote_file_path: str, 166 | content: t.Union[t.BinaryIO, bytes, str], 167 | timeout: t.Optional[float] = None, 168 | ) -> "RemoteFile": 169 | from .types import RemoteFile 170 | 171 | if isinstance(content, str): 172 | content = content.encode("utf-8") 173 | response = await self.aclient.post( 174 | url="/files/upload", 175 | files={"file": (remote_file_path, content)}, 176 | timeout=timeout, 177 | ) 178 | response.raise_for_status() 179 | return RemoteFile(path=remote_file_path, remote=self) 180 | 181 | @retry( 182 | retry=retry_if_exception( 183 | lambda e: isinstance(e, httpx.HTTPStatusError) 184 | and e.response.status_code == 502 185 | ), 186 | wait=wait_exponential(multiplier=1, min=5, max=150), 187 | stop=stop_after_attempt(3), 188 | ) 189 | def stream_download( 190 | self, 191 | remote_file_path: str, 192 | timeout: t.Optional[float] = None, 193 | ) -> t.Generator[bytes, None, None]: 194 | with self.client.stream( 195 | method="GET", 196 | url=f"/files/download/{remote_file_path}", 197 | timeout=timeout, 198 | ) as response: 199 | response.raise_for_status() 200 | for chunk in response.iter_bytes(): 201 | yield chunk 202 | 203 | @retry( 204 | retry=retry_if_exception( 205 | lambda e: isinstance(e, httpx.HTTPStatusError) 206 | and e.response.status_code == 502 207 | ), 208 | wait=wait_exponential(multiplier=1, min=5, max=150), 209 | stop=stop_after_attempt(3), 210 | ) 211 | async def astream_download( 212 | self, 213 | remote_file_path: str, 214 | timeout: t.Optional[float] = None, 215 | ) -> t.AsyncGenerator[bytes, None]: 216 | async with self.aclient.stream( 217 | method="GET", 218 | url=f"/files/download/{remote_file_path}", 219 | timeout=timeout, 220 | ) as response: 221 | response.raise_for_status() 222 | async for chunk in response.aiter_bytes(): 223 | yield chunk 224 | -------------------------------------------------------------------------------- /src/codeboxapi/types.py: -------------------------------------------------------------------------------- 1 | import typing as t 2 | from dataclasses import dataclass 3 | 4 | from .codebox import CodeBox 5 | 6 | 7 | @dataclass 8 | class RemoteFile: 9 | path: str 10 | remote: CodeBox 11 | _size: t.Optional[int] = None 12 | _content: t.Optional[bytes] = None 13 | 14 | @property 15 | def name(self) -> str: 16 | return self.path.split("/")[-1] 17 | 18 | def get_content(self) -> bytes: 19 | if self._content is None: 20 | self._content = b"".join(self.remote.stream_download(self.path)) 21 | return self._content 22 | 23 | async def aget_content(self) -> bytes: 24 | if self._content is None: 25 | self._content = b"" 26 | async for chunk in self.remote.astream_download(self.path): 27 | self._content += chunk 28 | return self._content 29 | 30 | def get_size(self) -> int: 31 | if self._size is None: 32 | self._size = len(self.get_content()) 33 | return self._size 34 | 35 | async def aget_size(self) -> int: 36 | if self._size is None: 37 | self._size = len(await self.aget_content()) 38 | return self._size 39 | 40 | def save(self, local_path: str) -> None: 41 | with open(local_path, "wb") as f: 42 | for chunk in self.remote.stream_download(self.path): 43 | f.write(chunk) 44 | 45 | async def asave(self, local_path: str) -> None: 46 | try: 47 | import aiofiles # type: ignore 48 | except ImportError: 49 | raise RuntimeError( 50 | "aiofiles is not installed. Please install it with " 51 | '`pip install "codeboxapi[local]"`' 52 | ) 53 | 54 | async with aiofiles.open(local_path, "wb") as f: 55 | async for chunk in self.remote.astream_download(self.path): 56 | await f.write(chunk) 57 | 58 | def __str__(self) -> str: 59 | return self.name 60 | 61 | def __repr__(self) -> str: 62 | if self._size is None: 63 | return f"RemoteFile({self.path})" 64 | return f"RemoteFile({self.path}, {self._size} bytes)" 65 | 66 | 67 | @dataclass 68 | class ExecChunk: 69 | """ 70 | A chunk of output from an execution. 71 | The type is one of: 72 | - txt: text output 73 | - img: image output 74 | - err: error output 75 | """ 76 | 77 | type: t.Literal["txt", "img", "err"] 78 | content: str 79 | 80 | 81 | @dataclass 82 | class ExecResult: 83 | chunks: list[ExecChunk] 84 | 85 | @property 86 | def text(self) -> str: 87 | return "".join(chunk.content for chunk in self.chunks if chunk.type == "txt") 88 | 89 | @property 90 | def images(self) -> list[str]: 91 | return [chunk.content for chunk in self.chunks if chunk.type == "img"] 92 | 93 | @property 94 | def errors(self) -> list[str]: 95 | return [chunk.content for chunk in self.chunks if chunk.type == "err"] 96 | 97 | 98 | @dataclass 99 | class CodeBoxOutput: 100 | """Deprecated CodeBoxOutput class""" 101 | 102 | content: str 103 | type: t.Literal["stdout", "stderr", "image/png", "error"] 104 | 105 | def __str__(self) -> str: 106 | return self.content 107 | 108 | def __eq__(self, other: object) -> bool: 109 | if isinstance(other, str): 110 | return self.content == other 111 | if isinstance(other, CodeBoxOutput): 112 | return self.content == other.content and self.type == other.type 113 | return False 114 | 115 | 116 | class CodeBoxFile: 117 | """Deprecated CodeBoxFile class""" 118 | 119 | def __init__(self, name: str, content: t.Optional[bytes] = None) -> None: 120 | from .utils import deprecated 121 | 122 | deprecated( 123 | "The CodeBoxFile class is deprecated. Use RemoteFile for file handling " 124 | "or plain bytes for content instead." 125 | )(lambda: None)() 126 | self.name = name 127 | self.content = content 128 | 129 | @classmethod 130 | def from_path(cls, path: str) -> "CodeBoxFile": 131 | import os 132 | 133 | with open(path, "rb") as f: 134 | return cls(name=os.path.basename(path), content=f.read()) 135 | -------------------------------------------------------------------------------- /src/codeboxapi/utils.py: -------------------------------------------------------------------------------- 1 | import os 2 | import signal 3 | import typing as t 4 | from contextlib import asynccontextmanager, contextmanager 5 | from functools import partial, reduce, wraps 6 | from importlib.metadata import PackageNotFoundError, distribution 7 | from warnings import warn 8 | 9 | import anyio 10 | import typing_extensions as te 11 | from anyio._core._eventloop import threadlocals 12 | 13 | if t.TYPE_CHECKING: 14 | from .types import ExecChunk, ExecResult 15 | 16 | T = t.TypeVar("T") 17 | P = te.ParamSpec("P") 18 | 19 | 20 | def deprecated(message: str) -> t.Callable[[t.Callable[P, T]], t.Callable[P, T]]: 21 | def decorator(func: t.Callable[P, T]) -> t.Callable[P, T]: 22 | @wraps(func) 23 | def wrapper(*args: P.args, **kwargs: P.kwargs) -> T: 24 | if os.getenv("IGNORE_DEPRECATION_WARNINGS", "false").lower() == "true": 25 | return func(*args, **kwargs) 26 | warn( 27 | f"{func.__name__} is deprecated. {message}", 28 | DeprecationWarning, 29 | stacklevel=2, 30 | ) 31 | return func(*args, **kwargs) 32 | 33 | return wrapper 34 | 35 | return decorator 36 | 37 | 38 | def resolve_pathlike(file: t.Union[str, os.PathLike]) -> str: 39 | if isinstance(file, os.PathLike): 40 | with open(file, "r") as f: 41 | return f.read() 42 | return file 43 | 44 | 45 | def reduce_bytes(async_gen: t.Iterator[bytes]) -> bytes: 46 | return reduce(lambda x, y: x + y, async_gen) 47 | 48 | 49 | def flatten_exec_result( 50 | result: t.Union["ExecResult", t.Iterator["ExecChunk"]], 51 | ) -> "ExecResult": 52 | from .types import ExecResult 53 | 54 | if not isinstance(result, ExecResult): 55 | result = ExecResult(chunks=[c for c in result]) 56 | # todo todo todo todo todo todo 57 | # remove empty text chunks 58 | # merge text chunks 59 | # remove empty stream chunks 60 | # merge stream chunks 61 | # remove empty error chunks 62 | # merge error chunks 63 | # ... 64 | return result 65 | 66 | 67 | async def async_flatten_exec_result( 68 | async_gen: t.AsyncGenerator["ExecChunk", None], 69 | ) -> "ExecResult": 70 | # todo todo todo todo todo todo 71 | # remove empty text chunks 72 | # merge text chunks 73 | # remove empty stream chunks 74 | # merge stream chunks 75 | # remove empty error chunks 76 | # merge error chunks 77 | # ... 78 | from .types import ExecResult 79 | 80 | return ExecResult(chunks=[c async for c in async_gen]) 81 | 82 | 83 | def syncify( 84 | async_function: t.Callable[P, t.Coroutine[t.Any, t.Any, T]], 85 | ) -> t.Callable[P, T]: 86 | """ 87 | Take an async function and create a regular one that receives the same keyword and 88 | positional arguments, and that when called, calls the original async function in 89 | the main async loop from the worker thread using `anyio.to_thread.run()`. 90 | """ 91 | 92 | @wraps(async_function) 93 | def wrapper(*args: P.args, **kwargs: P.kwargs) -> T: 94 | partial_f = partial(async_function, *args, **kwargs) 95 | 96 | if not getattr(threadlocals, "current_async_backend", None): 97 | return anyio.run(partial_f) 98 | return anyio.from_thread.run(partial_f) 99 | 100 | return wrapper 101 | 102 | 103 | def check_installed(package: str) -> None: 104 | """ 105 | Check if the given package is installed. 106 | """ 107 | try: 108 | distribution(package) 109 | except PackageNotFoundError: 110 | if os.getenv("DEBUG", "false").lower() == "true": 111 | print( 112 | f"\nMake sure '{package}' is installed " 113 | "when using without a CODEBOX_API_KEY.\n" 114 | f"You can install it with 'pip install {package}'.\n" 115 | ) 116 | raise 117 | 118 | 119 | @contextmanager 120 | def raise_timeout(timeout: t.Optional[float] = None): 121 | def timeout_handler(signum, frame): 122 | raise TimeoutError("Execution timed out") 123 | 124 | if timeout is not None: 125 | signal.signal(signal.SIGALRM, timeout_handler) 126 | signal.alarm(int(timeout)) 127 | 128 | try: 129 | yield 130 | finally: 131 | if timeout is not None: 132 | signal.alarm(0) 133 | 134 | 135 | @asynccontextmanager 136 | async def async_raise_timeout(timeout: t.Optional[float] = None): 137 | def timeout_handler(signum, frame): 138 | raise TimeoutError("Execution timed out") 139 | 140 | if timeout is not None: 141 | signal.signal(signal.SIGALRM, timeout_handler) 142 | signal.alarm(int(timeout)) 143 | 144 | try: 145 | yield 146 | finally: 147 | if timeout is not None: 148 | signal.alarm(0) 149 | 150 | 151 | @contextmanager 152 | def run_inside(directory: str): 153 | old_cwd = os.getcwd() 154 | os.chdir(directory) 155 | try: 156 | yield 157 | finally: 158 | os.chdir(old_cwd) 159 | 160 | 161 | def raise_error(message: str) -> t.NoReturn: 162 | raise Exception(message) 163 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import pytest 4 | from dotenv import load_dotenv 5 | 6 | from codeboxapi import CodeBox 7 | 8 | LOCALBOX = CodeBox(api_key="local") 9 | 10 | load_dotenv() 11 | 12 | 13 | @pytest.fixture( 14 | scope="session", 15 | params=["local", "docker", os.getenv("CODEBOX_API_KEY")], 16 | ) 17 | def codebox(request: pytest.FixtureRequest) -> CodeBox: 18 | if request.param == "local": 19 | return LOCALBOX 20 | 21 | if request.param == "docker" and ( 22 | os.system("docker ps > /dev/null 2>&1") != 0 23 | or os.getenv("GITHUB_ACTIONS") == "true" 24 | ): 25 | pytest.skip("Docker is not running") 26 | 27 | return CodeBox(api_key=request.param) 28 | -------------------------------------------------------------------------------- /tests/test_v01.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from codeboxapi import CodeBox 4 | 5 | 6 | def test_sync(codebox: CodeBox) -> None: 7 | try: 8 | assert codebox.start() == "started" 9 | print("Started") 10 | 11 | assert codebox.status() == "running" 12 | print("Running") 13 | 14 | codebox.run("x = 'Hello World!'") 15 | assert codebox.run("print(x)") == "Hello World!\n" 16 | print("Printed") 17 | 18 | file_name = "test_file.txt" 19 | assert file_name in str(codebox.upload(file_name, b"Hello World!")) 20 | print("Uploaded") 21 | 22 | assert file_name in str( 23 | codebox.run("import os;\nprint(os.listdir(os.getcwd())); ") 24 | ) 25 | assert file_name in str(codebox.list_files()) 26 | 27 | assert codebox.download(file_name).get_content() == b"Hello World!" 28 | print("Downloaded") 29 | 30 | assert "matplotlib" in str(codebox.install("matplotlib")) 31 | 32 | assert ( 33 | "error" 34 | != codebox.run("import matplotlib; print(matplotlib.__version__)").type 35 | ) 36 | print("Installed") 37 | 38 | o = codebox.run( 39 | "import matplotlib.pyplot as plt;" 40 | "plt.plot([1, 2, 3, 4], [1, 4, 2, 3]); plt.show()" 41 | ) 42 | assert o.type == "image/png" 43 | print("Plotted") 44 | 45 | finally: 46 | assert codebox.stop() == "stopped" 47 | print("Stopped") 48 | 49 | 50 | @pytest.mark.asyncio 51 | async def test_async(codebox: CodeBox) -> None: 52 | try: 53 | assert await codebox.astart() == "started" 54 | print("Started") 55 | 56 | assert await codebox.astatus() == "running" 57 | print("Running") 58 | 59 | await codebox.arun("x = 'Hello World!'") 60 | assert (await codebox.arun("print(x)")) == "Hello World!\n" 61 | print("Printed") 62 | 63 | file_name = "test_file.txt" 64 | assert file_name in str(await codebox.aupload(file_name, b"Hello World!")) 65 | print("Uploaded") 66 | 67 | assert file_name in str( 68 | await codebox.arun("import os;\nprint(os.listdir(os.getcwd())); ") 69 | ) 70 | 71 | assert file_name in str(await codebox.alist_files()) 72 | 73 | assert (await codebox.adownload(file_name)).get_content() == b"Hello World!" 74 | print("Downloaded") 75 | 76 | assert "matplotlib" in str(await codebox.ainstall("matplotlib")) 77 | 78 | assert ( 79 | "error" 80 | != ( 81 | await codebox.arun("import matplotlib; print(matplotlib.__version__)") 82 | ).type 83 | ) 84 | print("Installed") 85 | 86 | o = await codebox.arun( 87 | "import matplotlib.pyplot as plt;" 88 | "plt.plot([1, 2, 3, 4], [1, 4, 2, 3]); plt.show()" 89 | ) 90 | assert o.type == "image/png" 91 | print("Plotted") 92 | 93 | finally: 94 | assert await codebox.astop() == "stopped" 95 | print("Stopped") 96 | -------------------------------------------------------------------------------- /tests/test_v02.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | import pytest 4 | 5 | from codeboxapi import CodeBox, ExecChunk, ExecResult, RemoteFile 6 | 7 | 8 | def test_sync_codebox_lifecycle(codebox: CodeBox): 9 | assert codebox.healthcheck() == "healthy", "CodeBox should be healthy" 10 | 11 | result = codebox.exec("print('Hello World!')") 12 | assert isinstance(result, ExecResult), "Exec should return an ExecResult" 13 | assert result.text.strip() == "Hello World!", "Exec should print 'Hello World!'" 14 | assert not result.errors, "Exec should not produce errors" 15 | 16 | file_name = "test_file.txt" 17 | file_content = b"Hello World!" 18 | codebox.upload(file_name, file_content) 19 | 20 | downloaded_file = codebox.download(file_name) 21 | assert isinstance(downloaded_file, RemoteFile), ( 22 | "Download should return a RemoteFile" 23 | ) 24 | assert downloaded_file.get_content() == file_content, ( 25 | "Downloaded content should match uploaded content" 26 | ) 27 | 28 | install_result = codebox.install("matplotlib") 29 | assert "matplotlib" in install_result, "Matplotlib should be installed successfully" 30 | 31 | exec_result = codebox.exec("import matplotlib; print(matplotlib.__version__)") 32 | assert exec_result.errors == [], "Importing matplotlib should not produce errors" 33 | assert exec_result.text.strip() != "", "Matplotlib version should be printed" 34 | 35 | plot_result = codebox.exec( 36 | "import matplotlib.pyplot as plt; " 37 | "plt.figure(figsize=(10, 5)); " 38 | "plt.plot([1, 2, 3, 4], [1, 4, 2, 3]); " 39 | "plt.title('Test Plot'); " 40 | "plt.xlabel('X-axis'); " 41 | "plt.ylabel('Y-axis'); " 42 | "plt.show()" 43 | ) 44 | assert plot_result.images, "Plot execution should produce an image" 45 | assert len(plot_result.images) == 1, ( 46 | "Plot execution should produce exactly one image" 47 | ) 48 | 49 | 50 | @pytest.mark.asyncio 51 | async def test_async_codebox_lifecycle(codebox: CodeBox): 52 | assert await codebox.ahealthcheck() == "healthy", "CodeBox should be healthy" 53 | 54 | result = await codebox.aexec("print('Hello World!')") 55 | assert isinstance(result, ExecResult), "Exec should return an ExecResult" 56 | assert result.text.strip() == "Hello World!", "Exec should print 'Hello World!'" 57 | assert not result.errors, "Exec should not produce errors" 58 | 59 | file_name = "test_file.txt" 60 | file_content = b"Hello World!" 61 | await codebox.aupload(file_name, file_content) 62 | 63 | downloaded_file = await codebox.adownload(file_name) 64 | assert isinstance(downloaded_file, RemoteFile), ( 65 | "Download should return a RemoteFile" 66 | ) 67 | assert downloaded_file.get_content() == file_content, ( 68 | "Downloaded content should match uploaded content" 69 | ) 70 | 71 | install_result = await codebox.ainstall("matplotlib") 72 | assert "matplotlib" in install_result, "Matplotlib should be installed successfully" 73 | 74 | exec_result = await codebox.aexec( 75 | "import matplotlib; print(matplotlib.__version__)" 76 | ) 77 | assert exec_result.errors == [], "Importing matplotlib should not produce errors" 78 | assert exec_result.text.strip() != "", "Matplotlib version should be printed" 79 | 80 | plot_result = await codebox.aexec( 81 | "import matplotlib.pyplot as plt; " 82 | "plt.figure(figsize=(10, 5)); " 83 | "plt.plot([1, 2, 3, 4], [1, 4, 2, 3]); " 84 | "plt.title('Test Plot'); " 85 | "plt.xlabel('X-axis'); " 86 | "plt.ylabel('Y-axis'); " 87 | "plt.show()" 88 | ) 89 | assert plot_result.images, "Plot execution should produce an image" 90 | assert len(plot_result.images) == 1, ( 91 | "Plot execution should produce exactly one image" 92 | ) 93 | 94 | 95 | def test_sync_list_operations(codebox: CodeBox): 96 | codebox.exec("x = 1; y = 'test'; z = [1, 2, 3]") 97 | variables = codebox.show_variables() 98 | assert "x" in variables.keys(), "Variable 'x' should be listed" 99 | assert "1" in variables["x"], "Variable 'x' should contain value '1'" 100 | assert "y" in variables.keys(), "Variable 'y' should be listed" 101 | assert "test" in variables["y"], "Variable 'y' should contain value 'test'" 102 | assert "z" in variables.keys(), "Variable 'z' should be listed" 103 | assert "[1, 2, 3]" in variables["z"], "Variable 'z' should contain value '[1, 2, 3]" 104 | 105 | files = codebox.list_files() 106 | assert isinstance(files, list), "list_files should return a list" 107 | assert all(isinstance(f, RemoteFile) for f in files), ( 108 | "All items in list_files should be RemoteFile instances" 109 | ) 110 | 111 | packages = codebox.list_packages() 112 | assert isinstance(packages, list), "list_packages should return a list" 113 | assert len(packages) > 0, "There should be at least one package installed" 114 | assert any("matplotlib" in pkg for pkg in packages), ( 115 | "Matplotlib should be in the list of packages" 116 | ) 117 | 118 | 119 | @pytest.mark.asyncio 120 | async def test_async_list_operations(codebox: CodeBox): 121 | await codebox.aexec("x = 1; y = 'test'; z = [1, 2, 3]") 122 | variables = await codebox.ashow_variables() 123 | assert "x" in variables.keys(), "Variable 'x' should be listed" 124 | assert "1" in variables["x"], "Variable 'x' should contain value '1'" 125 | assert "y" in variables.keys(), "Variable 'y' should be listed" 126 | assert "test" in variables["y"], "Variable 'y' should contain value 'test'" 127 | assert "z" in variables.keys(), "Variable 'z' should be listed" 128 | assert "[1, 2, 3]" in variables["z"], ( 129 | "Variable 'z' should contain value '[1, 2, 3]'" 130 | ) 131 | 132 | files = await codebox.alist_files() 133 | assert isinstance(files, list), "list_files should return a list" 134 | assert all(isinstance(f, RemoteFile) for f in files), ( 135 | "All items in list_files should be RemoteFile instances" 136 | ) 137 | 138 | packages = await codebox.alist_packages() 139 | assert isinstance(packages, list), "list_packages should return a list" 140 | assert len(packages) > 0, "There should be at least one package installed" 141 | assert any("matplotlib" in pkg for pkg in packages), ( 142 | "Matplotlib should be in the list of packages" 143 | ) 144 | 145 | 146 | def test_sync_stream_exec(codebox: CodeBox): 147 | chunks: list[tuple[ExecChunk, float]] = [] 148 | t0 = time.perf_counter() 149 | sleep = 0.01 if codebox.api_key == "local" else 1 150 | for chunk in codebox.stream_exec( 151 | f"import time;\nfor i in range(3): time.sleep({sleep}); print(i)" 152 | ): 153 | chunks.append((chunk, time.perf_counter() - t0)) 154 | 155 | assert len(chunks) == 3, "iterating over stream_exec should produce 3 chunks" 156 | assert all(isinstance(chunk[0], ExecChunk) for chunk in chunks), ( 157 | "All items should be ExecChunk instances" 158 | ) 159 | assert all(chunk[0].type == "txt" for chunk in chunks), ( 160 | "All chunks should be of type 'txt'" 161 | ) 162 | assert [chunk[0].content.strip() for chunk in chunks] == [ 163 | "0", 164 | "1", 165 | "2", 166 | ], "Chunks should contain correct content" 167 | # Verify chunks arrive with delay 168 | assert all(chunks[i][1] < chunks[i + 1][1] for i in range(len(chunks) - 1)), ( 169 | "Chunks should arrive with delay" 170 | ) 171 | # Verify chunks don't arrive all at once 172 | assert any( 173 | chunks[i + 1][1] - chunks[i][1] > 0.005 for i in range(len(chunks) - 1) 174 | ), "At least some chunks should have noticeable delay between them" 175 | 176 | 177 | @pytest.mark.asyncio 178 | async def test_sync_stream_exec_ipython(codebox: CodeBox): 179 | chunks = [] 180 | t0 = time.perf_counter() 181 | sleep = 0.01 if codebox.api_key == "local" else 1 182 | for chunk in codebox.stream_exec( 183 | f"python -u -c 'import time\nfor i in range(3): time.sleep({sleep}); print(i)'", 184 | kernel="bash", 185 | ): 186 | chunks.append((chunk, time.perf_counter() - t0)) 187 | 188 | assert len(chunks) == 3, "iterating over stream_exec should produce 3 chunks" 189 | assert all(isinstance(chunk[0], ExecChunk) for chunk in chunks), ( 190 | "All items should be ExecChunk instances" 191 | ) 192 | assert all(chunk[0].type == "txt" for chunk in chunks), ( 193 | "All chunks should be of type 'txt'" 194 | ) 195 | assert [chunk[0].content.strip() for chunk in chunks] == [ 196 | "0", 197 | "1", 198 | "2", 199 | ], "Chunks should contain correct content" 200 | # Verify chunks arrive with delay 201 | assert all(chunks[i][1] < chunks[i + 1][1] for i in range(len(chunks) - 1)), ( 202 | "Chunks should arrive with delay" 203 | ) 204 | # Verify chunks don't arrive all at once 205 | assert any( 206 | chunks[i + 1][1] - chunks[i][1] > 0.005 for i in range(len(chunks) - 1) 207 | ), "At least some chunks should have noticeable delay between them" 208 | 209 | 210 | @pytest.mark.asyncio 211 | async def test_async_stream_exec_ipython(codebox: CodeBox): 212 | chunks: list[tuple[ExecChunk, float]] = [] 213 | t0 = time.perf_counter() 214 | sleep = 0.01 if codebox.api_key == "local" else 1 215 | async for chunk in codebox.astream_exec( 216 | f"import time;\nfor i in range(3): time.sleep({sleep}); print(i)", 217 | ): 218 | chunks.append((chunk, time.perf_counter() - t0)) 219 | 220 | assert len(chunks) == 3, "iterating over stream_exec should produce 3 chunks" 221 | assert all(isinstance(chunk[0], ExecChunk) for chunk in chunks), ( 222 | "All items should be ExecChunk instances" 223 | ) 224 | assert all(chunk[0].type == "txt" for chunk in chunks), ( 225 | "All chunks should be of type 'txt'" 226 | ) 227 | assert [chunk[0].content.strip() for chunk in chunks] == [ 228 | "0", 229 | "1", 230 | "2", 231 | ], "Chunks should contain correct content" 232 | # Verify chunks arrive with delay 233 | assert all(chunks[i][1] < chunks[i + 1][1] for i in range(len(chunks) - 1)), ( 234 | "Chunks should arrive with delay" 235 | ) 236 | # Verify chunks don't arrive all at once 237 | assert any( 238 | chunks[i + 1][1] - chunks[i][1] > 0.005 for i in range(len(chunks) - 1) 239 | ), "At least some chunks should have noticeable delay between them" 240 | 241 | 242 | @pytest.mark.asyncio 243 | async def test_async_stream_exec_bash(codebox: CodeBox): 244 | chunks = [] 245 | t0 = time.perf_counter() 246 | sleep = 0.01 if codebox.api_key == "local" else 1 247 | async for chunk in codebox.astream_exec( 248 | f"python -u -c 'import time\nfor i in range(3): time.sleep({sleep}); print(i)'", 249 | kernel="bash", 250 | ): 251 | chunks.append((chunk, time.perf_counter() - t0)) 252 | 253 | assert len(chunks) == 3, "iterating over stream_exec should produce 3 chunks" 254 | assert all(isinstance(chunk[0], ExecChunk) for chunk in chunks), ( 255 | "All items should be ExecChunk instances" 256 | ) 257 | assert all(chunk[0].type == "txt" for chunk in chunks), ( 258 | "All chunks should be of type 'txt'" 259 | ) 260 | assert [chunk[0].content.strip() for chunk in chunks] == [ 261 | "0", 262 | "1", 263 | "2", 264 | ], "Chunks should contain correct content" 265 | # Verify chunks arrive with delay 266 | assert all(chunks[i][1] < chunks[i + 1][1] for i in range(len(chunks) - 1)), ( 267 | "Chunks should arrive with delay" 268 | ) 269 | # Verify chunks don't arrive all at once 270 | assert any( 271 | chunks[i + 1][1] - chunks[i][1] > 0.005 for i in range(len(chunks) - 1) 272 | ), "At least some chunks should have noticeable delay between them" 273 | 274 | 275 | def test_sync_error_handling(codebox: CodeBox): 276 | result = codebox.exec("1/0") 277 | assert result.errors, "Execution should produce an error" 278 | error = result.errors[0].lower() 279 | assert "division" in error and "zero" in error, ( 280 | "Error should be a ZeroDivisionError" 281 | ) 282 | 283 | 284 | @pytest.mark.asyncio 285 | async def test_async_error_handling(codebox: CodeBox): 286 | result = await codebox.aexec("1/0") 287 | assert result.errors, "Execution should produce an error" 288 | error = result.errors[0].lower() 289 | assert "division" in error and "zero" in error, ( 290 | "Error should be a ZeroDivisionError" 291 | ) 292 | 293 | 294 | def test_sync_bash_commands(codebox: CodeBox): 295 | result = codebox.exec("echo ok", kernel="bash") 296 | assert "ok" in result.text, "Execution should contain 'ok'" 297 | result = codebox.exec("echo \"print('Hello!')\" > test.py", kernel="bash") 298 | assert result.text.strip() == "", "Execution result should be empty" 299 | assert "test.py" in [file.path for file in codebox.list_files()] 300 | result = codebox.exec("python test.py", kernel="bash") 301 | assert result.text.strip() == "Hello!", "Execution result should be 'Hello!'" 302 | 303 | 304 | @pytest.mark.asyncio 305 | async def test_async_bash_commands(codebox: CodeBox): 306 | result = await codebox.aexec("echo ok", kernel="bash") 307 | assert "ok" in result.text, "Execution should contain 'ok'" 308 | result = await codebox.aexec("echo 'print(\"Hello!\")' > test.py", kernel="bash") 309 | assert result.text.strip() == "", "Execution result should be empty" 310 | assert "test.py" in [file.path for file in await codebox.alist_files()] 311 | result = await codebox.aexec("python test.py", kernel="bash") 312 | assert result.text.strip() == "Hello!", "Execution result should be 'Hello!'" 313 | 314 | 315 | def test_file_from_url(codebox: CodeBox): 316 | url = "https://raw.githubusercontent.com/shroominic/codebox-api/main/README.md" 317 | file_path = "README.md" 318 | remote_file = codebox.file_from_url(url, file_path) 319 | assert isinstance(remote_file, RemoteFile), "Should return a RemoteFile" 320 | assert remote_file.path == file_path, "File path should match" 321 | assert len(remote_file.get_content()) > 0, "File should have content" 322 | assert file_path in [file.path for file in codebox.list_files()] 323 | 324 | 325 | @pytest.mark.asyncio 326 | async def test_file_from_url_async(codebox: CodeBox): 327 | url = "https://raw.githubusercontent.com/shroominic/codebox-api/main/README.md" 328 | file_path = "README.md" 329 | remote_file = await codebox.afile_from_url(url, file_path) 330 | assert isinstance(remote_file, RemoteFile), "Should return a RemoteFile" 331 | assert remote_file.path == file_path, "File path should match" 332 | assert len(remote_file.get_content()) > 0, "File should have content" 333 | assert file_path in [file.path for file in await codebox.alist_files()] 334 | 335 | 336 | def test_local_box_singleton(): 337 | from codeboxapi.local import LocalBox 338 | 339 | with pytest.raises(RuntimeError) as exc_info: 340 | _ = LocalBox() 341 | 342 | assert "Only one LocalBox instance can exist at a time" in str(exc_info.value) 343 | 344 | with pytest.raises(RuntimeError) as exc_info: 345 | _ = CodeBox(api_key="local") 346 | 347 | assert "codeboxapi.com" in str(exc_info.value) 348 | 349 | 350 | if __name__ == "__main__": 351 | pytest.main([__file__]) 352 | --------------------------------------------------------------------------------