63 Commits

Author SHA1 Message Date
github-actions
563e41f213 0.2.0
Automatically generated by python-semantic-release
2025-05-21 17:32:50 +00:00
31e09bc7ba ci: try fixing the dynamic_import issue (2, force adding pyproject.toml) 2025-05-21 11:31:17 -06:00
252d8be735 ci: try fixing the dynamic_import issue 2025-05-21 11:25:05 -06:00
fbba8b7613 ci: add semantic release configuration (and use pyproject version) 2025-05-21 11:11:40 -06:00
75daccb366 feat(cubbix): add --no-shell in combination with --run to not drop a shell and exit when the command is done 2025-04-30 09:46:28 -06:00
e852fdd1fb doc: fix url 2025-04-25 00:17:28 +02:00
e34fc56df4 doc: remove .git of the clone url 2025-04-25 00:11:48 +02:00
ba852d502e docs: add information for uvx 2025-04-18 11:10:29 -06:00
15d86d25e7 docs: update README 2025-04-18 11:09:19 -06:00
5218bb1218 docs: update classifiers 2025-04-18 10:58:23 -06:00
13c896a58d docs: add mit license 2025-04-18 10:55:23 -06:00
github-actions
ea0d44f4db 0.1.0-rc.1
Automatically generated by python-semantic-release
2025-04-18 15:35:36 +00:00
3850bc3212 ci: add ci files (#11)
* ci: add ci files

* fix: add goose image build
2025-04-18 17:33:25 +02:00
12d77d0128 refactor: rename project to cubbi 2025-04-17 20:33:20 -06:00
51fb79baa3 refactor: rename driver to image, first pass 2025-04-17 17:33:51 -06:00
3799f04c13 fix: mcp tests 2025-04-13 18:30:02 -06:00
7fc9cfd8e1 fix: osx tests on volume 2025-04-13 18:20:33 -06:00
979b43846a refactor: reduce amount of data in session.yaml 2025-04-09 11:52:02 -06:00
4f54c0fbe7 fix: remove the "mc stop" meant to be in the container, but not implemented 2025-04-09 11:40:33 -06:00
3a182fd265 feat(project): explicitely add --project to save information in /mc-config across run.
Containers are now isolated by default.
2025-04-04 17:16:26 -06:00
2f9fd68cad feat(gemini): support for gemini model 2025-04-03 16:11:27 -06:00
e25e30e749 fix(uid): correctly pass uid/gid to project 2025-04-02 17:21:56 -06:00
b1aa415dde fix(goose): always update the file 2025-04-02 16:55:33 -06:00
5b9713dc2f feat(llm): add default model/provider to auto configure the driver (#7) 2025-04-03 00:11:53 +02:00
9e742b439b feat(goose): update config using uv script with pyyaml (#6) 2025-04-02 23:27:37 +02:00
cfa7dd647d fix(goose): ensure configuration is run as user 2025-04-01 19:37:58 -06:00
deff036406 fix(mcp): fix UnboundLocalError: cannot access local variable 'container_name' where it is not associated with a value 2025-04-01 19:00:34 -06:00
5678438661 feat(ssh): make SSH server optional with --ssh flag
- Added --ssh flag to session create command
- Modified mc-init.sh to check MC_SSH_ENABLED environment variable
- SSH server is now disabled by default
- Updated README.md with new flag example
- Fixed UnboundLocalError with container_name in exception handler
2025-04-01 18:58:06 -06:00
30c6b995cb chore: remove unnecessary output 2025-04-01 17:11:14 -06:00
f32b3dd269 fix(ssh): do not enable ssh automatically 2025-04-01 17:08:52 -06:00
a74251b119 fix(uid): use symlink instead of volume for persistent volume in the container 2025-04-01 17:01:25 -06:00
9c21611a7f docs: Prefer mcx alias in README examples 2025-04-01 09:54:16 -06:00
6b2c1ebf1c docs: Add --run option examples to README 2025-04-01 09:54:16 -06:00
33d90d0531 feat(run): add --run command 2025-04-01 09:54:16 -06:00
a51115a45d feat(mc): support for uid/gid, and use default current user 2025-04-01 09:54:16 -06:00
0d75bfc3d8 feat(mcp): ensure inner mcp environemnt variables are passed 2025-04-01 09:54:16 -06:00
7805aa720e feat(goose): auto add mcp server to goose configuration when starting a session 2025-03-25 23:48:08 +01:00
16f59b1c40 feat(goose): optimize init status 2025-03-25 23:20:19 +01:00
4b0461a6fa feat(mcp): add the possibility to have default mcp to connect to 2025-03-25 23:20:19 +01:00
5d674f7508 fix(session): ensure a session connect only to the mcp server passed in --mcp 2025-03-25 23:20:19 +01:00
3ee8ce6338 feat(mcp): improve inspector reliability over re-run 2025-03-25 23:20:19 +01:00
d098f268cd feat(mcp): add inspector 2025-03-25 23:20:19 +01:00
0892b6c8c4 feat(mcp): first docker proxy working 2025-03-25 23:20:19 +01:00
212f271268 feat(mcp): initial version of mcp 2025-03-25 23:20:19 +01:00
20916c5713 docs(mcp): add specification for MCP server support 2025-03-12 18:45:06 -06:00
7c46d66b53 test: add unit tests 2025-03-12 18:44:48 -06:00
2caeb42551 feat(volume): add mc config volume command 2025-03-12 12:01:00 -06:00
deb5945e40 feat(config): ensure config is correctly saved 2025-03-11 23:02:50 -06:00
7736573b84 feat(cli): separate session state into its own session.yaml file 2025-03-11 22:58:31 -06:00
133583b941 feat(cli): support to join external network 2025-03-11 22:37:46 -06:00
028bd26cf1 fix(goose): add ping, nano and vim to the default image 2025-03-11 22:35:02 -06:00
7649173d6c fix(goose): install latest goose version, do not use pip 2025-03-11 20:33:27 -06:00
307eee4fce refactor: move drivers directory into mcontainer package
- Relocate goose driver to mcontainer/drivers/
- Update ConfigManager to dynamically scan for driver YAML files
- Add support for mc-driver.yaml instead of mai-driver.yaml
- Update Driver model to support init commands and other YAML fields
- Auto-discover drivers at runtime instead of hardcoding them
- Update documentation to reflect new directory structure
2025-03-11 20:12:05 -06:00
6f08e2b274 fix(mc): fix runtime issue when starting mc 2025-03-11 16:04:13 -06:00
b72f1eef9a feat(volume): add the possibilty to mount local directory into the container (like docker volume) 2025-03-11 15:58:13 -06:00
092f497ecc fix(session): fix session status display 2025-03-11 15:35:57 -06:00
dab783b01d feat(config): add global user configuration for the tool
- langfuse
- default driver
- and api keys
2025-03-11 12:47:18 -06:00
d42af870ff fix(goose): remove MCP_HOST and such, this is not how mcp works 2025-03-11 12:15:40 -06:00
e36eef4ef7 fix(langfuse): fix goose langfuse integration (wrong env variables) 2025-03-11 12:12:20 -06:00
f83c49c0f3 feat(keys): pass local keys to the session by default 2025-03-11 11:39:21 -06:00
e36f4540bf fix: remove double connecting to message 2025-03-11 11:28:25 -06:00
1c538f8a59 docs(readme): remove license part 2025-03-11 10:51:25 -06:00
a4591ddbd8 docs(readme): update README to update tool call 2025-03-11 10:50:27 -06:00
45 changed files with 1961 additions and 1283 deletions

View File

@@ -0,0 +1,17 @@
name: Conventional commit PR
on: [pull_request]
jobs:
cog_check_job:
runs-on: ubuntu-latest
name: check conventional commit compliance
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
# pick the pr HEAD instead of the merge commit
ref: ${{ github.event.pull_request.head.sha }}
- name: Conventional commit check
uses: cocogitto/cocogitto-action@v3

View File

@@ -0,0 +1,21 @@
name: "Lint PR"
on:
pull_request_target:
types:
- opened
- edited
- synchronize
- reopened
permissions:
pull-requests: read
jobs:
main:
name: Validate PR title
runs-on: ubuntu-latest
steps:
- uses: amannn/action-semantic-pull-request@v5
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

14
.github/workflows/pre_commit.yml vendored Normal file
View File

@@ -0,0 +1,14 @@
name: pre-commit
on:
pull_request:
push:
branches: [main]
jobs:
pre-commit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v3
- uses: pre-commit/action@v3.0.1

40
.github/workflows/pytests.yml vendored Normal file
View File

@@ -0,0 +1,40 @@
name: Pytests
on:
pull_request:
push:
permissions:
contents: write
checks: write
pull-requests: write
jobs:
pytest:
runs-on: ubuntu-latest
strategy:
fail-fast: true
matrix:
python-version: ["3.12"]
steps:
- uses: actions/checkout@v4
- name: Install uv
uses: astral-sh/setup-uv@v4
with:
enable-cache: true
python-version: ${{ matrix.python-version }}
- name: Install all dependencies
run: uv sync --frozen --all-extras --all-groups
- name: Build goose image
run: |
uv tool install --with-editable . .
cubbi image build goose
- name: Tests
run: |
uv run --frozen -m pytest -v

127
.github/workflows/release.yml vendored Normal file
View File

@@ -0,0 +1,127 @@
name: Release
on:
workflow_dispatch:
inputs:
release_force:
# see https://python-semantic-release.readthedocs.io/en/latest/github-action.html#command-line-options
description: |
Force release be one of: [major | minor | patch]
Leave empty for auto-detect based on commit messages.
type: choice
options:
- "" # auto - no force
- major # force major
- minor # force minor
- patch # force patch
default: ""
required: false
prerelease_token:
description: 'The "prerelease identifier" to use as a prefix for the "prerelease" part of a semver. Like the rc in `1.2.0-rc.8`.'
type: choice
options:
- rc
- beta
- alpha
default: rc
required: false
prerelease:
description: "Is a pre-release"
type: boolean
default: false
required: false
concurrency:
group: deploy
cancel-in-progress: false # prevent hickups with semantic-release
env:
PYTHON_VERSION_DEFAULT: "3.12"
jobs:
release:
runs-on: ubuntu-latest
concurrency: release
permissions:
id-token: write
contents: write
steps:
# Note: we need to checkout the repository at the workflow sha in case during the workflow
# the branch was updated. To keep PSR working with the configured release branches,
# we force a checkout of the desired release branch but at the workflow sha HEAD.
- name: Setup | Checkout Repository at workflow sha
uses: actions/checkout@v4
with:
fetch-depth: 0
ref: ${{ github.sha }}
ssh-key: ${{ secrets.DEPLOY_KEY }}
- name: Setup | Force correct release branch on workflow sha
run: |
git checkout -B ${{ github.ref_name }} ${{ github.sha }}
- name: Install uv
uses: astral-sh/setup-uv@v4
with:
enable-cache: true
python-version: ${{ env.PYTHON_VERSION_DEFAULT }}
- name: Install all dependencies
run: uv sync --frozen --all-extras --all-groups
# 2 steps to prevent uv.lock out of sync
# CF https://github.com/python-semantic-release/python-semantic-release/issues/1125
- name: Action | Semantic Version Release (stamp only)
uses: python-semantic-release/python-semantic-release@v9.21.1
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
git_committer_name: "github-actions"
git_committer_email: "actions@users.noreply.github.com"
force: ${{ github.event.inputs.release_force }}
prerelease: ${{ github.event.inputs.prerelease }}
prerelease_token: ${{ github.event.inputs.prerelease_token }}
ssh_public_signing_key: ${{ secrets.DEPLOY_KEY_PUB }}
ssh_private_signing_key: ${{ secrets.DEPLOY_KEY }}
push: false
commit: false
tag: false
changelog: false
- name: Push and tags
run: |
uv lock
git add uv.lock pyproject.toml
- name: Action | Semantic Version Release (fully to create release)
id: release
uses: python-semantic-release/python-semantic-release@v9.21.1
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
git_committer_name: "github-actions"
git_committer_email: "actions@users.noreply.github.com"
force: ${{ github.event.inputs.release_force }}
prerelease: ${{ github.event.inputs.prerelease }}
prerelease_token: ${{ github.event.inputs.prerelease_token }}
ssh_public_signing_key: ${{ secrets.DEPLOY_KEY_PUB }}
ssh_private_signing_key: ${{ secrets.DEPLOY_KEY }}
push: false
- name: Push and tags
run: |
git push --set-upstream --follow-tags origin ${{ github.ref_name }}
- name: Build package
run: uv build
- name: Publish | Upload package to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
if: steps.release.outputs.released == 'true'
- name: Publish | Upload to GitHub Release Assets
uses: python-semantic-release/publish-action@v9.8.9
if: steps.release.outputs.released == 'true'
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
tag: ${{ steps.release.outputs.tag }}

1
.gitignore vendored
View File

@@ -12,3 +12,4 @@ wheels/
# Aider
.aider*
.goose
.claude/settings.local.json

249
CHANGELOG.md Normal file
View File

@@ -0,0 +1,249 @@
# CHANGELOG
## v0.2.0 (2025-05-21)
### Continuous Integration
- Add semantic release configuration (and use pyproject version)
([`fbba8b7`](https://github.com/Monadical-SAS/cubbi/commit/fbba8b7613c76c6a1ae21c81d9f07697320f6d10))
- Try fixing the dynamic_import issue
([`252d8be`](https://github.com/Monadical-SAS/cubbi/commit/252d8be735e6d18761c42e9c138ccafde89fd6ee))
- Try fixing the dynamic_import issue (2, force adding pyproject.toml)
([`31e09bc`](https://github.com/Monadical-SAS/cubbi/commit/31e09bc7ba8446508a90f5a9423271ac386498fe))
### Documentation
- Add information for uvx
([`ba852d5`](https://github.com/Monadical-SAS/cubbi/commit/ba852d502eea4fc558c0f96d9015436101d5ef43))
- Add mit license
([`13c896a`](https://github.com/Monadical-SAS/cubbi/commit/13c896a58d9bc6f25b0688f9ae7117ae868ae705))
- Update classifiers
([`5218bb1`](https://github.com/Monadical-SAS/cubbi/commit/5218bb121804c440dc69c9d932787ed6d54b90f5))
- Update README
([`15d86d2`](https://github.com/Monadical-SAS/cubbi/commit/15d86d25e74162153c26d6c254059f24d46c4095))
### Features
- **cubbix**: Add --no-shell in combination with --run to not drop a shell and exit when the command
is done
([`75daccb`](https://github.com/Monadical-SAS/cubbi/commit/75daccb3662d059d178fd0f12026bb97f29f2452))
## v0.1.0-rc.1 (2025-04-18)
### Bug Fixes
- Mcp tests
([`3799f04`](https://github.com/Monadical-SAS/cubbi/commit/3799f04c1395d3b018f371db0c0cb8714e6fb8b3))
- Osx tests on volume
([`7fc9cfd`](https://github.com/Monadical-SAS/cubbi/commit/7fc9cfd8e1babfa069691d3b7997449535069674))
- Remove double connecting to message
([`e36f454`](https://github.com/Monadical-SAS/cubbi/commit/e36f4540bfe3794ab2d065f552cfb9528489de71))
- Remove the "mc stop" meant to be in the container, but not implemented
([`4f54c0f`](https://github.com/Monadical-SAS/cubbi/commit/4f54c0fbe7886c8551368b4b35be3ad8c7ae49ab))
- **cli**: Rename MAI->MC
([`354834f`](https://github.com/Monadical-SAS/cubbi/commit/354834fff733c37202b01a6fc49ebdf5003390c1))
- **goose**: Add ping, nano and vim to the default image
([`028bd26`](https://github.com/Monadical-SAS/cubbi/commit/028bd26cf12e181541e006650b58d97e1d568a45))
- **goose**: Always update the file
([`b1aa415`](https://github.com/Monadical-SAS/cubbi/commit/b1aa415ddee981dc1278cd24f7509363b9c54a54))
- **goose**: Ensure configuration is run as user
([`cfa7dd6`](https://github.com/Monadical-SAS/cubbi/commit/cfa7dd647d1e4055bf9159be2ee9c2280f2d908e))
- **goose**: Install latest goose version, do not use pip
([`7649173`](https://github.com/Monadical-SAS/cubbi/commit/7649173d6c8a82ac236d0f89263591eaa6e21a20))
- **goose**: Remove MCP_HOST and such, this is not how mcp works
([`d42af87`](https://github.com/Monadical-SAS/cubbi/commit/d42af870ff56112b4503f2568b8a5b0f385c435c))
- **goose**: Rename mai to mc, add initialization status
([`74c723d`](https://github.com/Monadical-SAS/cubbi/commit/74c723db7b6b7dd57c4ca32a804436a990e5260c))
- **langfuse**: Fix goose langfuse integration (wrong env variables)
([`e36eef4`](https://github.com/Monadical-SAS/cubbi/commit/e36eef4ef7c2d0cbdef31704afb45c50c4293986))
- **mc**: Fix runtime issue when starting mc
([`6f08e2b`](https://github.com/Monadical-SAS/cubbi/commit/6f08e2b274b67001694123b5bb977401df0810c6))
- **mcp**: Fix UnboundLocalError: cannot access local variable 'container_name' where it is not
associated with a value
([`deff036`](https://github.com/Monadical-SAS/cubbi/commit/deff036406d72d55659da40520a3a09599d65f07))
- **session**: Ensure a session connect only to the mcp server passed in --mcp
([`5d674f7`](https://github.com/Monadical-SAS/cubbi/commit/5d674f750878f0895dc1544620e8b1da4da29752))
- **session**: Fix session status display
([`092f497`](https://github.com/Monadical-SAS/cubbi/commit/092f497ecc19938d4917a18441995170d1f68704))
- **ssh**: Do not enable ssh automatically
([`f32b3dd`](https://github.com/Monadical-SAS/cubbi/commit/f32b3dd269d1a3d6ebaa2e7b2893f267b5175b20))
- **uid**: Correctly pass uid/gid to project
([`e25e30e`](https://github.com/Monadical-SAS/cubbi/commit/e25e30e7492c6b0a03017440a18bb2708927fc19))
- **uid**: Use symlink instead of volume for persistent volume in the container
([`a74251b`](https://github.com/Monadical-SAS/cubbi/commit/a74251b119d24714c7cc1eaadeea851008006137))
### Chores
- Remove unnecessary output
([`30c6b99`](https://github.com/Monadical-SAS/cubbi/commit/30c6b995cbb5bdf3dc7adf2e79d8836660d4f295))
- Update doc and add pre-commit
([`958d87b`](https://github.com/Monadical-SAS/cubbi/commit/958d87bcaeed16210a7c22574b5e63f2422af098))
### Continuous Integration
- Add ci files ([#11](https://github.com/Monadical-SAS/cubbi/pull/11),
[`3850bc3`](https://github.com/Monadical-SAS/cubbi/commit/3850bc32129da539f53b69427ddca85f8c5f390a))
* ci: add ci files
* fix: add goose image build
### Documentation
- Add --run option examples to README
([`6b2c1eb`](https://github.com/Monadical-SAS/cubbi/commit/6b2c1ebf1cd7a5d9970234112f32fe7a231303f9))
- Prefer mcx alias in README examples
([`9c21611`](https://github.com/Monadical-SAS/cubbi/commit/9c21611a7fa1497f7cbddb1f1b4cd22b4ebc8a19))
- **mcp**: Add specification for MCP server support
([`20916c5`](https://github.com/Monadical-SAS/cubbi/commit/20916c5713b3a047f4a8a33194f751f36e3c8a7a))
- **readme**: Remove license part
([`1c538f8`](https://github.com/Monadical-SAS/cubbi/commit/1c538f8a59e28888309c181ae8f8034b9e70a631))
- **readme**: Update README to update tool call
([`a4591dd`](https://github.com/Monadical-SAS/cubbi/commit/a4591ddbd863bc6658a7643d3f33d06c82816cae))
### Features
- First commit
([`fde6529`](https://github.com/Monadical-SAS/cubbi/commit/fde6529d545b5625484c5c1236254d2e0c6f0f4d))
- **cli**: Auto connect to a session
([`4a63606`](https://github.com/Monadical-SAS/cubbi/commit/4a63606d58cc3e331a349974e9b3bf2d856a72a1))
- **cli**: Auto mount current directory as /app
([`e6e3c20`](https://github.com/Monadical-SAS/cubbi/commit/e6e3c207bcee531b135824688adf1a56ae427a01))
- **cli**: More information when closing session
([`08ba1ab`](https://github.com/Monadical-SAS/cubbi/commit/08ba1ab2da3c24237c0f0bc411924d8ffbe71765))
- **cli**: Phase 1 - local cli with docker integration
([`6443083`](https://github.com/Monadical-SAS/cubbi/commit/64430830d883308e4d52e17b25c260a0d5385141))
- **cli**: Separate session state into its own session.yaml file
([`7736573`](https://github.com/Monadical-SAS/cubbi/commit/7736573b84c7a51eaa60b932f835726b411ca742))
- **cli**: Support to join external network
([`133583b`](https://github.com/Monadical-SAS/cubbi/commit/133583b941ed56d1b0636277bb847c45eee7f3b8))
- **config**: Add global user configuration for the tool
([`dab783b`](https://github.com/Monadical-SAS/cubbi/commit/dab783b01d82bcb210b5e01ac3b93ba64c7bc023))
- langfuse - default driver - and api keys
- **config**: Ensure config is correctly saved
([`deb5945`](https://github.com/Monadical-SAS/cubbi/commit/deb5945e40d55643dca4e1aa4201dfa8da1bfd70))
- **gemini**: Support for gemini model
([`2f9fd68`](https://github.com/Monadical-SAS/cubbi/commit/2f9fd68cada9b5aaba652efb67368c2641046da5))
- **goose**: Auto add mcp server to goose configuration when starting a session
([`7805aa7`](https://github.com/Monadical-SAS/cubbi/commit/7805aa720eba78d47f2ad565f6944e84a21c4b1c))
- **goose**: Optimize init status
([`16f59b1`](https://github.com/Monadical-SAS/cubbi/commit/16f59b1c408dbff4781ad7ccfa70e81d6d98f7bd))
- **goose**: Update config using uv script with pyyaml
([#6](https://github.com/Monadical-SAS/cubbi/pull/6),
[`9e742b4`](https://github.com/Monadical-SAS/cubbi/commit/9e742b439b7b852efa4219850f8b67c143274045))
- **keys**: Pass local keys to the session by default
([`f83c49c`](https://github.com/Monadical-SAS/cubbi/commit/f83c49c0f340d1a3accba1fe1317994b492755c0))
- **llm**: Add default model/provider to auto configure the driver
([#7](https://github.com/Monadical-SAS/cubbi/pull/7),
[`5b9713d`](https://github.com/Monadical-SAS/cubbi/commit/5b9713dc2f7d7c25808ad37094838c697c056fec))
- **mc**: Support for uid/gid, and use default current user
([`a51115a`](https://github.com/Monadical-SAS/cubbi/commit/a51115a45d88bf703fb5380171042276873b7207))
- **mcp**: Add inspector
([`d098f26`](https://github.com/Monadical-SAS/cubbi/commit/d098f268cd164e9d708089c9f9525a940653c010))
- **mcp**: Add the possibility to have default mcp to connect to
([`4b0461a`](https://github.com/Monadical-SAS/cubbi/commit/4b0461a6faf81de1e1b54d1fe78fea7977cde9dd))
- **mcp**: Ensure inner mcp environemnt variables are passed
([`0d75bfc`](https://github.com/Monadical-SAS/cubbi/commit/0d75bfc3d8e130fb05048c2bc8a674f6b7e5de83))
- **mcp**: First docker proxy working
([`0892b6c`](https://github.com/Monadical-SAS/cubbi/commit/0892b6c8c472063c639cc78cf29b322bb39f998f))
- **mcp**: Improve inspector reliability over re-run
([`3ee8ce6`](https://github.com/Monadical-SAS/cubbi/commit/3ee8ce6338c35b7e48d788d2dddfa9b6a70381cb))
- **mcp**: Initial version of mcp
([`212f271`](https://github.com/Monadical-SAS/cubbi/commit/212f271268c5724775beceae119f97aec2748dcb))
- **project**: Explicitely add --project to save information in /mc-config across run.
([`3a182fd`](https://github.com/Monadical-SAS/cubbi/commit/3a182fd2658c0eb361ce5ed88938686e2bd19e59))
Containers are now isolated by default.
- **run**: Add --run command
([`33d90d0`](https://github.com/Monadical-SAS/cubbi/commit/33d90d05311ad872b7a7d4cd303ff6f7b7726038))
- **ssh**: Make SSH server optional with --ssh flag
([`5678438`](https://github.com/Monadical-SAS/cubbi/commit/56784386614fcd0a52be8a2eb89d2deef9323ca1))
- Added --ssh flag to session create command - Modified mc-init.sh to check MC_SSH_ENABLED
environment variable - SSH server is now disabled by default - Updated README.md with new flag
example - Fixed UnboundLocalError with container_name in exception handler
- **volume**: Add mc config volume command
([`2caeb42`](https://github.com/Monadical-SAS/cubbi/commit/2caeb425518242fbe1c921b9678e6e7571b9b0a6))
- **volume**: Add the possibilty to mount local directory into the container (like docker volume)
([`b72f1ee`](https://github.com/Monadical-SAS/cubbi/commit/b72f1eef9af598f2090a0edae8921c16814b3cda))
### Refactoring
- Move drivers directory into mcontainer package
([`307eee4`](https://github.com/Monadical-SAS/cubbi/commit/307eee4fcef47189a98a76187d6080a36423ad6e))
- Relocate goose driver to mcontainer/drivers/ - Update ConfigManager to dynamically scan for driver
YAML files - Add support for mc-driver.yaml instead of mai-driver.yaml - Update Driver model to
support init commands and other YAML fields - Auto-discover drivers at runtime instead of
hardcoding them - Update documentation to reflect new directory structure
- Reduce amount of data in session.yaml
([`979b438`](https://github.com/Monadical-SAS/cubbi/commit/979b43846a798f1fb25ff05e6dc1fc27fa16f590))
- Rename driver to image, first pass
([`51fb79b`](https://github.com/Monadical-SAS/cubbi/commit/51fb79baa30ff479ac5479ba5ea0cad70bbb4c20))
- Rename project to cubbi
([`12d77d0`](https://github.com/Monadical-SAS/cubbi/commit/12d77d0128e4d82e5ddc1a4ab7e873ddaa22e130))
### Testing
- Add unit tests
([`7c46d66`](https://github.com/Monadical-SAS/cubbi/commit/7c46d66b53ac49c08458bc5d72e636e7d296e74f))

View File

@@ -1,15 +1,12 @@
# Monadical Container Development Guide
# Cubbi Container Development Guide
## Build Commands
```bash
# Install dependencies using uv (Astral)
uv sync
# Run MC service
uv run -m mcontainer.service
# Run MC CLI
uv run -m mcontainer.cli
# Run Cubbi CLI
uv run -m cubbi.cli
```
## Lint/Test Commands

9
LICENSE Normal file
View File

@@ -0,0 +1,9 @@
MIT License
Copyright (c) 2025 Monadical SAS
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

269
README.md
View File

@@ -1,124 +1,154 @@
# MC - Monadical Container Tool
<div align="center">
MC (Monadical Container) is a command-line tool for managing ephemeral
containers that run AI tools and development environments. It works with both
local Docker and a dedicated remote web service that manages containers in a
Docker-in-Docker (DinD) environment. MC also supports connecting to MCP (Model Context Protocol) servers to extend AI tools with additional capabilities.
# Cubbi - Container Tool
## Quick Reference
Cubbi is a command-line tool for managing ephemeral containers that run AI tools and development environments. It works with both local Docker and a dedicated remote web service that manages containers in a Docker-in-Docker (DinD) environment. Cubbi also supports connecting to MCP (Model Control Protocol) servers to extend AI tools with additional capabilities.
- `mc session create` - Create a new session
- `mcx` - Shortcut for `mc session create`
- `mcx .` - Mount the current directory
- `mcx /path/to/dir` - Mount a specific directory
- `mcx https://github.com/user/repo` - Clone a repository
![PyPI - Version](https://img.shields.io/pypi/v/cubbi)
![PyPI - Python Version](https://img.shields.io/pypi/pyversions/cubbi)
[![Tests](https://github.com/monadical-sas/cubbi/actions/workflows/pytests.yml/badge.svg?branch=main&event=push)](https://github.com/monadical-sas/cubbi/actions/workflows/pytests.yml)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
## Requirements
</div>
- [uv](https://docs.astral.sh/uv/)
## 🚀 Quick Reference
## Installation
- `cubbi session create` - Create a new session
- `cubbix` - Shortcut for `cubbi session create`
- `cubbix .` - Mount the current directory
- `cubbix /path/to/dir` - Mount a specific directory
- `cubbix https://github.com/user/repo` - Clone a repository
## 📋 Requirements
- [Docker](https://www.docker.com/)
- [uv](https://astral.sh/uv)
## 📥 Installation
```bash
# Clone the repository
git clone https://github.com/monadical/mcontainer.git
# Via pip
pip install cubbi
# Install the tool locally
# (with editable, so you can update the code and work with it)
cd mcontainer
uv tool install --with-editable . .
# Via uv
uv tool install cubbi
# Then you could use the tool as `mc`
mc --help
# Without installation
# (meaning all commands below must be prefixed with `uvx`)
uvx cubbi
```
## Basic Usage
Then compile your first image:
```bash
cubbi image build goose
```
### For Developers
If you are looking to contribute to the development, you will need to use `uv` as well:
```bash
git clone https://github.com/monadical-sas/cubbi
cd cubbi
uv tool install --with-editable . .
# You'll have cubbi and cubbix executable files in your PATH, pointing to the local installation.
```
## 📚 Basic Usage
```bash
# Show help message (displays available commands)
mc
cubbi
# Create a new session with the default driver (using mcx alias)
mcx
# Create a new session with the default image (using cubbix alias)
cubbix
# Create a session and run an initial command before the shell starts
mcx --run "echo 'Setup complete'; ls -l"
cubbix --run "ls -l"
# Create a session, run a command, and exit (no shell prompt)
cubbix --run "ls -l" --no-shell
# List all active sessions
mc session list
cubbi session list
# Connect to a specific session
mc session connect SESSION_ID
cubbi session connect SESSION_ID
# Close a session when done
mc session close SESSION_ID
cubbi session close SESSION_ID
# Create a session with a specific driver
mcx --driver goose
# Create a session with a specific image
cubbix --image goose
# Create a session with environment variables
mcx -e VAR1=value1 -e VAR2=value2
cubbix -e VAR1=value1 -e VAR2=value2
# Mount custom volumes (similar to Docker's -v flag)
mcx -v /local/path:/container/path
mcx -v ~/data:/data -v ./configs:/etc/app/config
cubbix -v /local/path:/container/path
cubbix -v ~/data:/data -v ./configs:/etc/app/config
# Mount a local directory (current directory or specific path)
mcx .
mcx /path/to/project
cubbix .
cubbix /path/to/project
# Connect to external Docker networks
mcx --network teamnet --network dbnet
cubbix --network teamnet --network dbnet
# Connect to MCP servers for extended capabilities
mcx --mcp github --mcp jira
cubbix --mcp github --mcp jira
# Clone a Git repository
mcx https://github.com/username/repo
cubbix https://github.com/username/repo
# Using the mcx shortcut (equivalent to mc session create)
mcx # Creates a session without mounting anything
mcx . # Mounts the current directory
mcx /path/to/project # Mounts the specified directory
mcx https://github.com/username/repo # Clones the repository
# Using the cubbix shortcut (equivalent to cubbi session create)
cubbix # Creates a session without mounting anything
cubbix . # Mounts the current directory
cubbix /path/to/project # Mounts the specified directory
cubbix https://github.com/username/repo # Clones the repository
# Shorthand with MCP servers
mcx https://github.com/username/repo --mcp github
cubbix https://github.com/username/repo --mcp github
# Shorthand with an initial command
mcx . --run "apt-get update && apt-get install -y my-package"
cubbix . --run "apt-get update && apt-get install -y my-package"
# Execute a command and exit without starting a shell
cubbix . --run "python script.py" --no-shell
# Enable SSH server in the container
mcx --ssh
cubbix --ssh
```
## Driver Management
## 🖼️ Image Management
MC includes a driver management system that allows you to build, manage, and use Docker images for different AI tools:
Cubbi includes an image management system that allows you to build, manage, and use Docker images for different AI tools:
```bash
# List available drivers
mc driver list
# List available images
cubbi image list
# Get detailed information about a driver
mc driver info goose
# Get detailed information about an image
cubbi image info goose
# Build a driver image
mc driver build goose
# Build an image
cubbi image build goose
# Build and push a driver image
mc driver build goose --push
# Build and push an image
cubbi image build goose --push
```
Drivers are defined in the `mcontainer/drivers/` directory, with each subdirectory containing:
Images are defined in the `cubbi/images/` directory, with each subdirectory containing:
- `Dockerfile`: Docker image definition
- `entrypoint.sh`: Container entrypoint script
- `mc-init.sh`: Standardized initialization script
- `mc-driver.yaml`: Driver metadata and configuration
- `README.md`: Driver documentation
- `cubbi-init.sh`: Standardized initialization script
- `cubbi-image.yaml`: Image metadata and configuration
- `README.md`: Image documentation
MC automatically discovers and loads driver definitions from the YAML files.
Cubbi automatically discovers and loads image definitions from the YAML files.
```
## Development
@@ -129,37 +159,34 @@ uv run -m pytest
# Run linting
uvx ruff check .
# Run type checking
uvx mypy .
# Format code
uvx ruff format .
```
## Configuration
## ⚙️ Configuration
MC supports user-specific configuration via a YAML file located at `~/.config/mc/config.yaml`. This allows you to set default values and configure service credentials.
Cubbi supports user-specific configuration via a YAML file located at `~/.config/cubbi/config.yaml`. This allows you to set default values and configure service credentials.
### Managing Configuration
```bash
# View all configuration
mc config list
cubbi config list
# Get a specific configuration value
mc config get langfuse.url
cubbi config get langfuse.url
# Set configuration values
mc config set langfuse.url "https://cloud.langfuse.com"
mc config set langfuse.public_key "pk-lf-..."
mc config set langfuse.secret_key "sk-lf-..."
cubbi config set langfuse.url "https://cloud.langfuse.com"
cubbi config set langfuse.public_key "pk-lf-..."
cubbi config set langfuse.secret_key "sk-lf-..."
# Set API keys for various services
mc config set openai.api_key "sk-..."
mc config set anthropic.api_key "sk-ant-..."
cubbi config set openai.api_key "sk-..."
cubbi config set anthropic.api_key "sk-ant-..."
# Reset configuration to defaults
mc config reset
cubbi config reset
```
### Default Networks Configuration
@@ -168,13 +195,13 @@ You can configure default networks that will be applied to every new session:
```bash
# List default networks
mc config network list
cubbi config network list
# Add a network to defaults
mc config network add teamnet
cubbi config network add teamnet
# Remove a network from defaults
mc config network remove teamnet
cubbi config network remove teamnet
```
### Default Volumes Configuration
@@ -183,13 +210,13 @@ You can configure default volumes that will be automatically mounted in every ne
```bash
# List default volumes
mc config volume list
cubbi config volume list
# Add a volume to defaults
mc config volume add /local/path:/container/path
cubbi config volume add /local/path:/container/path
# Remove a volume from defaults (will prompt if multiple matches found)
mc config volume remove /local/path
cubbi config volume remove /local/path
```
Default volumes will be combined with any volumes specified using the `-v` flag when creating a session.
@@ -200,35 +227,33 @@ You can configure default MCP servers that sessions will automatically connect t
```bash
# List default MCP servers
mc config mcp list
cubbi config mcp list
# Add an MCP server to defaults
mc config mcp add github
cubbi config mcp add github
# Remove an MCP server from defaults
mc config mcp remove github
cubbi config mcp remove github
```
When adding new MCP servers, they are added to defaults by default. Use the `--no-default` flag to prevent this:
```bash
# Add an MCP server without adding it to defaults
mc mcp add github ghcr.io/mcp/github:latest --no-default
mc mcp add-remote jira https://jira-mcp.example.com/sse --no-default
cubbi mcp add github -e GITHUB_PERSONAL_ACCESS_TOKEN=xxxx github mcp/github --no-default
```
When creating sessions, if no MCP server is specified with `--mcp`, the default MCP servers will be used automatically.
### External Network Connectivity
MC containers can connect to external Docker networks, allowing them to communicate with other services in those networks:
Cubbi containers can connect to external Docker networks, allowing them to communicate with other services in those networks:
```bash
# Create a session connected to external networks
mc session create --network teamnet --network dbnet
cubbi session create --network teamnet --network dbnet
```
**Important**: Networks must be "attachable" to be joined by MC containers. Here's how to create attachable networks:
**Important**: Networks must be "attachable" to be joined by Cubbi containers. Here's how to create attachable networks:
```bash
# Create an attachable network with Docker
@@ -246,12 +271,12 @@ services:
networks:
teamnet:
driver: bridge
attachable: true # This is required for MC containers to connect
attachable: true # This is required for Cubbi containers to connect
```
### Service Credentials
Service credentials like API keys configured in `~/.config/mc/config.yaml` are automatically passed to containers as environment variables:
Service credentials like API keys configured in `~/.config/cubbi/config.yaml` are automatically passed to containers as environment variables:
| Config Setting | Environment Variable |
|----------------|---------------------|
@@ -263,68 +288,65 @@ Service credentials like API keys configured in `~/.config/mc/config.yaml` are a
| `openrouter.api_key` | `OPENROUTER_API_KEY` |
| `google.api_key` | `GOOGLE_API_KEY` |
## MCP Server Management
## 🌐 MCP Server Management
MCP (Model Context Protocol) servers provide tool-calling capabilities to AI models, enhancing their ability to interact with external services, databases, and systems. MC supports multiple types of MCP servers:
MCP (Model Control Protocol) servers provide tool-calling capabilities to AI models, enhancing their ability to interact with external services, databases, and systems. Cubbi supports multiple types of MCP servers:
1. **Remote HTTP SSE servers** - External MCP servers accessed over HTTP
2. **Docker-based MCP servers** - Local MCP servers running in Docker containers
3. **Proxy-based MCP servers** - Local MCP servers with an SSE proxy for stdio-to-SSE conversion
2. **Docker-based MCP servers** - Local MCP servers running in Docker containers, with a SSE proxy for stdio-to-SSE conversion
### Managing MCP Servers
```bash
# List all configured MCP servers and their status
mc mcp list
cubbi mcp list
# View detailed status of an MCP server
mc mcp status github
cubbi mcp status github
# Start/stop/restart individual MCP servers
mc mcp start github
mc mcp stop github
mc mcp restart github
cubbi mcp start github
cubbi mcp stop github
cubbi mcp restart github
# Start all MCP servers at once
mc mcp start --all
cubbi mcp start --all
# Stop and remove all MCP servers at once
mc mcp stop --all
cubbi mcp stop --all
# Run the MCP Inspector to visualize and interact with MCP servers
# It automatically joins all MCP networks for seamless DNS resolution
# Uses two ports: frontend UI (default: 5173) and backend API (default: 3000)
mc mcp inspector
cubbi mcp inspector
# Run the MCP Inspector with custom ports
mc mcp inspector --client-port 6173 --server-port 6174
cubbi mcp inspector --client-port 6173 --server-port 6174
# Run the MCP Inspector in detached mode
mc mcp inspector --detach
cubbi mcp inspector --detach
# Stop the MCP Inspector
mc mcp inspector --stop
cubbi mcp inspector --stop
# View MCP server logs
mc mcp logs github
cubbi mcp logs github
# Remove an MCP server configuration
mc mcp remove github
cubbi mcp remove github
```
### Adding MCP Servers
MC supports different types of MCP servers:
Cubbi supports different types of MCP servers:
```bash
# Add a remote HTTP SSE MCP server
mc mcp remote add github http://my-mcp-server.example.com/sse --header "Authorization=Bearer token123"
# Example of docker-based MCP server
cubbi mcp add fetch mcp/fetch
cubbi mcp add github -e GITHUB_PERSONAL_ACCESS_TOKEN=xxxx github mcp/github
# Add a Docker-based MCP server
mc mcp docker add github mcp/github:latest --command "github-mcp" --env GITHUB_TOKEN=ghp_123456
# Add a proxy-based MCP server (for stdio-to-SSE conversion)
mc mcp add github ghcr.io/mcp/github:latest --proxy-image ghcr.io/sparfenyuk/mcp-proxy:latest --command "github-mcp" --sse-port 8080 --no-default
# Example of SSE-based MCP server
cubbi mcp add myserver https://myssemcp.com
```
### Using MCP Servers with Sessions
@@ -333,13 +355,14 @@ MCP servers can be attached to sessions when they are created:
```bash
# Create a session with a single MCP server
mc session create --mcp github
cubbi session create --mcp github
# Create a session with multiple MCP servers
mc session create --mcp github --mcp jira
# Using MCP with a project repository
mc github.com/username/repo --mcp github
cubbi session create --mcp github --mcp jira
```
MCP servers are persistent and can be shared between sessions. They continue running even when sessions are closed, allowing for efficient reuse across multiple sessions.
## 📜 License
Cubbi is licensed under the [MIT License](LICENSE).

View File

@@ -1,5 +1,5 @@
"""
MC - Monadical Container Tool
Cubbi - Cubbi Container Tool
"""
__version__ = "0.1.0"

View File

@@ -1,20 +1,21 @@
"""
CLI for Monadical Container Tool.
CLI for Cubbi Container Tool.
"""
import os
import logging
import os
from typing import List, Optional
import typer
from rich.console import Console
from rich.table import Table
from .config import ConfigManager
from .container import ContainerManager
from .models import SessionStatus
from .user_config import UserConfigManager
from .session import SessionManager
from .mcp import MCPManager
from .models import SessionStatus
from .session import SessionManager
from .user_config import UserConfigManager
# Configure logging - will only show logs if --verbose flag is used
logging.basicConfig(
@@ -23,13 +24,13 @@ logging.basicConfig(
handlers=[logging.StreamHandler()],
)
app = typer.Typer(help="Monadical Container Tool", no_args_is_help=True)
session_app = typer.Typer(help="Manage MC sessions", no_args_is_help=True)
driver_app = typer.Typer(help="Manage MC drivers", no_args_is_help=True)
config_app = typer.Typer(help="Manage MC configuration", no_args_is_help=True)
app = typer.Typer(help="Cubbi Container Tool", no_args_is_help=True)
session_app = typer.Typer(help="Manage Cubbi sessions", no_args_is_help=True)
image_app = typer.Typer(help="Manage Cubbi images", no_args_is_help=True)
config_app = typer.Typer(help="Manage Cubbi configuration", no_args_is_help=True)
mcp_app = typer.Typer(help="Manage MCP servers", no_args_is_help=True)
app.add_typer(session_app, name="session", no_args_is_help=True)
app.add_typer(driver_app, name="driver", no_args_is_help=True)
app.add_typer(image_app, name="image", no_args_is_help=True)
app.add_typer(config_app, name="config", no_args_is_help=True)
app.add_typer(mcp_app, name="mcp", no_args_is_help=True)
@@ -48,10 +49,10 @@ def main(
False, "--verbose", "-v", help="Enable verbose logging"
),
) -> None:
"""Monadical Container Tool
"""Cubbi Container Tool
Run 'mc session create' to create a new session.
Use 'mcx' as a shortcut for 'mc session create'.
Run 'cubbi session create' to create a new session.
Use 'cubbix' as a shortcut for 'cubbi session create'.
"""
# Set log level based on verbose flag
if verbose:
@@ -60,19 +61,19 @@ def main(
@app.command()
def version() -> None:
"""Show MC version information"""
"""Show Cubbi version information"""
from importlib.metadata import version as get_version
try:
version_str = get_version("mcontainer")
console.print(f"MC - Monadical Container Tool v{version_str}")
version_str = get_version("cubbi")
console.print(f"Cubbi - Cubbi Container Tool v{version_str}")
except Exception:
console.print("MC - Monadical Container Tool (development version)")
console.print("Cubbi - Cubbi Container Tool (development version)")
@session_app.command("list")
def list_sessions() -> None:
"""List active MC sessions"""
"""List active Cubbi sessions"""
sessions = container_manager.list_sessions()
if not sessions:
@@ -82,7 +83,7 @@ def list_sessions() -> None:
table = Table(show_header=True, header_style="bold")
table.add_column("ID")
table.add_column("Name")
table.add_column("Driver")
table.add_column("Image")
table.add_column("Status")
table.add_column("Ports")
@@ -110,7 +111,7 @@ def list_sessions() -> None:
table.add_row(
session.id,
session.name,
session.driver,
session.image,
f"[{status_color}]{status_name}[/{status_color}]",
ports_str,
)
@@ -120,7 +121,7 @@ def list_sessions() -> None:
@session_app.command("create")
def create_session(
driver: Optional[str] = typer.Option(None, "--driver", "-d", help="Driver to use"),
image: Optional[str] = typer.Option(None, "--image", "-i", help="Image to use"),
path_or_url: Optional[str] = typer.Argument(
None,
help="Local directory path to mount or repository URL to clone",
@@ -146,6 +147,11 @@ def create_session(
"--run",
help="Command to execute inside the container before starting the shell",
),
no_shell: bool = typer.Option(
False,
"--no-shell",
help="Close container after '--run' command finishes (only valid with --run)",
),
no_connect: bool = typer.Option(
False, "--no-connect", help="Don't automatically connect to the session"
),
@@ -166,8 +172,11 @@ def create_session(
None, "--provider", "-p", help="Provider to use"
),
ssh: bool = typer.Option(False, "--ssh", help="Start SSH server in the container"),
verbose: bool = typer.Option(
False, "--verbose", "-v", help="Enable verbose logging"
),
) -> None:
"""Create a new MC session
"""Create a new Cubbi session
If a local directory path is provided, it will be mounted at /app in the container.
If a repository URL is provided, it will be cloned into /app during initialization.
@@ -181,11 +190,13 @@ def create_session(
target_gid = gid if gid is not None else os.getgid()
console.print(f"Using UID: {target_uid}, GID: {target_gid}")
# Use default driver from user configuration
if not driver:
driver = user_config.get(
"defaults.driver", config_manager.config.defaults.get("driver", "goose")
# Use default image from user configuration
if not image:
image_name = user_config.get(
"defaults.image", config_manager.config.defaults.get("image", "goose")
)
else:
image_name = image
# Start with environment variables from user configuration
environment = user_config.get_environment_variables()
@@ -254,15 +265,21 @@ def create_session(
for host_path, mount_info in volume_mounts.items():
console.print(f" {host_path} -> {mount_info['bind']}")
with console.status(f"Creating session with driver '{driver}'..."):
with console.status(f"Creating session with image '{image_name}'..."):
# If path_or_url is a local directory, we should mount it
# If it's a Git URL or doesn't exist, handle accordingly
mount_local = False
if path_or_url and os.path.isdir(os.path.expanduser(path_or_url)):
mount_local = True
# Check if --no-shell is used without --run
if no_shell and not run_command:
console.print(
"[yellow]Warning: --no-shell is ignored without --run[/yellow]"
)
session = container_manager.create_session(
driver_name=driver,
image_name=image_name,
project=path_or_url,
project_name=project,
environment=environment,
@@ -272,6 +289,7 @@ def create_session(
networks=all_networks,
mcp=all_mcps,
run_command=run_command,
no_shell=no_shell,
uid=target_uid,
gid=target_gid,
ssh=ssh,
@@ -282,31 +300,69 @@ def create_session(
if session:
console.print("[green]Session created successfully![/green]")
console.print(f"Session ID: {session.id}")
console.print(f"Driver: {session.driver}")
console.print(f"Image: {session.image}")
if session.ports:
console.print("Ports:")
for container_port, host_port in session.ports.items():
console.print(f" {container_port} -> {host_port}")
# Auto-connect based on user config, unless overridden by --no-connect flag
# Auto-connect based on user config, unless overridden by --no-connect flag or --no-shell
auto_connect = user_config.get("defaults.connect", True)
# Connect if auto_connect is enabled and --no-connect wasn't used.
# The --run command no longer prevents connection.
should_connect = not no_connect and auto_connect
if should_connect:
container_manager.connect_session(session.id)
# When --no-shell is used with --run, show logs instead of connecting
if no_shell and run_command:
console.print(
"[yellow]Executing command and waiting for completion...[/yellow]"
)
console.print("Container will exit after command completes.")
console.print("[bold]Command logs:[/bold]")
# Stream logs from the container until it exits
container_manager.get_session_logs(session.id, follow=True)
# At this point the command and container should have finished
# Clean up the session entry to avoid leaving stale entries
with console.status("Cleaning up session..."):
# Give a short delay to ensure container has fully exited
import time
time.sleep(1)
# Remove the session from session manager
session_manager.remove_session(session.id)
try:
# Also try to remove the container to ensure no resources are left behind
container = container_manager.client.containers.get(
session.container_id
)
if container.status != "running":
container.remove(force=False)
except Exception as e:
# Container might already be gone or in the process of exiting
# This is fine, just log it
if verbose:
console.print(f"[yellow]Note: {e}[/yellow]")
console.print(
"[green]Command execution complete. Container has exited.[/green]"
)
console.print("[green]Session has been cleaned up.[/green]")
else:
# Explain why connection was skipped
if no_connect:
console.print("\nConnection skipped due to --no-connect.")
console.print(
f"Connect manually with:\n mc session connect {session.id}"
)
elif not auto_connect:
console.print(
f"\nAuto-connect disabled. Connect with:\n mc session connect {session.id}"
)
# Connect if auto_connect is enabled and --no-connect wasn't used.
# The --run command no longer prevents connection.
should_connect = not no_connect and auto_connect
if should_connect:
container_manager.connect_session(session.id)
else:
# Explain why connection was skipped
if no_connect:
console.print("\nConnection skipped due to --no-connect.")
console.print(
f"Connect manually with:\n cubbi session connect {session.id}"
)
elif not auto_connect:
console.print(
f"\nAuto-connect disabled. Connect with:\n cubbi session connect {session.id}"
)
else:
console.print("[red]Failed to create session[/red]")
@@ -316,7 +372,7 @@ def close_session(
session_id: Optional[str] = typer.Argument(None, help="Session ID to close"),
all_sessions: bool = typer.Option(False, "--all", help="Close all active sessions"),
) -> None:
"""Close a MC session or all sessions"""
"""Close a Cubbi session or all sessions"""
if all_sessions:
# Get sessions first to display them
sessions = container_manager.list_sessions()
@@ -361,7 +417,7 @@ def close_session(
def connect_session(
session_id: str = typer.Argument(..., help="Session ID to connect to"),
) -> None:
"""Connect to a MC session"""
"""Connect to a Cubbi session"""
console.print(f"Connecting to session {session_id}...")
success = container_manager.connect_session(session_id)
@@ -377,7 +433,7 @@ def session_logs(
False, "--init", "-i", help="Show initialization logs instead of container logs"
),
) -> None:
"""Stream logs from a MC session"""
"""Stream logs from a Cubbi session"""
if init:
# Show initialization logs
if follow:
@@ -402,13 +458,13 @@ def session_logs(
console.print(logs)
@driver_app.command("list")
def list_drivers() -> None:
"""List available MC drivers"""
drivers = config_manager.list_drivers()
@image_app.command("list")
def list_images() -> None:
"""List available Cubbi images"""
images = config_manager.list_images()
if not drivers:
console.print("No drivers found")
if not images:
console.print("No images found")
return
table = Table(show_header=True, header_style="bold")
@@ -418,92 +474,92 @@ def list_drivers() -> None:
table.add_column("Maintainer")
table.add_column("Image")
for name, driver in drivers.items():
for name, image in images.items():
table.add_row(
driver.name,
driver.description,
driver.version,
driver.maintainer,
driver.image,
image.name,
image.description,
image.version,
image.maintainer,
image.image,
)
console.print(table)
@driver_app.command("build")
def build_driver(
driver_name: str = typer.Argument(..., help="Driver name to build"),
@image_app.command("build")
def build_image(
image_name: str = typer.Argument(..., help="Image name to build"),
tag: str = typer.Option("latest", "--tag", "-t", help="Image tag"),
push: bool = typer.Option(
False, "--push", "-p", help="Push image to registry after building"
),
) -> None:
"""Build a driver Docker image"""
# Get driver path
driver_path = config_manager.get_driver_path(driver_name)
if not driver_path:
console.print(f"[red]Driver '{driver_name}' not found[/red]")
"""Build an image Docker image"""
# Get image path
image_path = config_manager.get_image_path(image_name)
if not image_path:
console.print(f"[red]Image '{image_name}' not found[/red]")
return
# Check if Dockerfile exists
dockerfile_path = driver_path / "Dockerfile"
dockerfile_path = image_path / "Dockerfile"
if not dockerfile_path.exists():
console.print(f"[red]Dockerfile not found in {driver_path}[/red]")
console.print(f"[red]Dockerfile not found in {image_path}[/red]")
return
# Build image name
image_name = f"monadical/mc-{driver_name}:{tag}"
docker_image_name = f"monadical/cubbi-{image_name}:{tag}"
# Build the image
with console.status(f"Building image {image_name}..."):
result = os.system(f"cd {driver_path} && docker build -t {image_name} .")
with console.status(f"Building image {docker_image_name}..."):
result = os.system(f"cd {image_path} && docker build -t {docker_image_name} .")
if result != 0:
console.print("[red]Failed to build driver image[/red]")
console.print("[red]Failed to build image[/red]")
return
console.print(f"[green]Successfully built image: {image_name}[/green]")
console.print(f"[green]Successfully built image: {docker_image_name}[/green]")
# Push if requested
if push:
with console.status(f"Pushing image {image_name}..."):
result = os.system(f"docker push {image_name}")
with console.status(f"Pushing image {docker_image_name}..."):
result = os.system(f"docker push {docker_image_name}")
if result != 0:
console.print("[red]Failed to push driver image[/red]")
console.print("[red]Failed to push image[/red]")
return
console.print(f"[green]Successfully pushed image: {image_name}[/green]")
console.print(f"[green]Successfully pushed image: {docker_image_name}[/green]")
@driver_app.command("info")
def driver_info(
driver_name: str = typer.Argument(..., help="Driver name to get info for"),
@image_app.command("info")
def image_info(
image_name: str = typer.Argument(..., help="Image name to get info for"),
) -> None:
"""Show detailed information about a driver"""
driver = config_manager.get_driver(driver_name)
if not driver:
console.print(f"[red]Driver '{driver_name}' not found[/red]")
"""Show detailed information about an image"""
image = config_manager.get_image(image_name)
if not image:
console.print(f"[red]Image '{image_name}' not found[/red]")
return
console.print(f"[bold]Driver: {driver.name}[/bold]")
console.print(f"Description: {driver.description}")
console.print(f"Version: {driver.version}")
console.print(f"Maintainer: {driver.maintainer}")
console.print(f"Image: {driver.image}")
console.print(f"[bold]Image: {image.name}[/bold]")
console.print(f"Description: {image.description}")
console.print(f"Version: {image.version}")
console.print(f"Maintainer: {image.maintainer}")
console.print(f"Docker Image: {image.image}")
if driver.ports:
if image.ports:
console.print("\n[bold]Ports:[/bold]")
for port in driver.ports:
for port in image.ports:
console.print(f" {port}")
# Get driver path
driver_path = config_manager.get_driver_path(driver_name)
if driver_path:
console.print(f"\n[bold]Path:[/bold] {driver_path}")
# Get image path
image_path = config_manager.get_image_path(image_name)
if image_path:
console.print(f"\n[bold]Path:[/bold] {image_path}")
# Check for README
readme_path = driver_path / "README.md"
readme_path = image_path / "README.md"
if readme_path.exists():
console.print("\n[bold]README:[/bold]")
with open(readme_path, "r") as f:
@@ -1469,7 +1525,7 @@ def run_mcp_inspector(
# If stop flag is set, stop all running MCP Inspectors
if stop:
containers = client.containers.list(
all=True, filters={"label": "mc.mcp.inspector=true"}
all=True, filters={"label": "cubbi.mcp.inspector=true"}
)
if not containers:
console.print("[yellow]No running MCP Inspector instances found[/yellow]")
@@ -1488,7 +1544,7 @@ def run_mcp_inspector(
# Check if inspector is already running
all_inspectors = client.containers.list(
all=True, filters={"label": "mc.mcp.inspector=true"}
all=True, filters={"label": "cubbi.mcp.inspector=true"}
)
# Stop any existing inspectors first
@@ -1532,7 +1588,7 @@ def run_mcp_inspector(
return
# Container name with timestamp to avoid conflicts
container_name = f"mc_mcp_inspector_{int(time.time())}"
container_name = f"cubbi_mcp_inspector_{int(time.time())}"
with console.status("Starting MCP Inspector..."):
# Get MCP servers from configuration
@@ -1565,7 +1621,7 @@ def run_mcp_inspector(
mcp_name = mcp.get("name")
try:
# Get the container name for this MCP
container_name = f"mc_mcp_{mcp_name}"
container_name = f"cubbi_mcp_{mcp_name}"
container = None
# Try to find the container
@@ -1616,7 +1672,7 @@ def run_mcp_inspector(
# Make sure we have at least one network to connect to
if not mcp_networks_to_connect:
# Create an MCP-specific network if none exists
network_name = "mc-mcp-network"
network_name = "cubbi-mcp-network"
console.print("No MCP networks found, creating a default one")
try:
networks = client.networks.list(names=[network_name])
@@ -1676,7 +1732,8 @@ exec npm start
# Write the script to a temp file
script_path = os.path.join(
os.path.dirname(os.path.abspath(__file__)), "mc_inspector_entrypoint.sh"
os.path.dirname(os.path.abspath(__file__)),
"cubbi_inspector_entrypoint.sh",
)
with open(script_path, "w") as f:
f.write(script_content)
@@ -1693,7 +1750,7 @@ exec npm start
# Check if existing container with the same name exists, and remove it
try:
existing = client.containers.get("mc_mcp_inspector")
existing = client.containers.get("cubbi_mcp_inspector")
if existing.status == "running":
existing.stop(timeout=1)
existing.remove(force=True)
@@ -1719,7 +1776,7 @@ exec npm start
for mcp in all_mcps:
if mcp.get("type") in ["docker", "proxy"]:
mcp_name = mcp.get("name")
container_name = f"mc_mcp_{mcp_name}"
container_name = f"cubbi_mcp_{mcp_name}"
try:
# Check if this container exists
@@ -1739,7 +1796,7 @@ exec npm start
container = client.containers.run(
image="mcp/inspector",
name="mc_mcp_inspector", # Use a fixed name
name="cubbi_mcp_inspector", # Use a fixed name
detach=True,
network=initial_network,
ports={
@@ -1762,8 +1819,8 @@ exec npm start
},
entrypoint="/entrypoint.sh",
labels={
"mc.mcp.inspector": "true",
"mc.managed": "true",
"cubbi.mcp.inspector": "true",
"cubbi.managed": "true",
},
network_mode=None, # Don't use network_mode as we're using network with aliases
networking_config=client.api.create_networking_config(network_config),
@@ -1799,7 +1856,7 @@ exec npm start
for mcp in all_mcps:
if mcp.get("type") in ["docker", "proxy"]:
mcp_name = mcp.get("name")
container_name = f"mc_mcp_{mcp_name}"
container_name = f"cubbi_mcp_{mcp_name}"
try:
# Check if this container exists
@@ -1871,7 +1928,7 @@ exec npm start
"[yellow]Warning: No MCP servers found or started. The Inspector will run but won't have any servers to connect to.[/yellow]"
)
console.print(
"Start MCP servers using 'mc mcp start --all' and then restart the Inspector."
"Start MCP servers using 'cubbi mcp start --all' and then restart the Inspector."
)
if not detach:
@@ -1887,19 +1944,19 @@ exec npm start
def session_create_entry_point():
"""Entry point that directly invokes 'mc session create'.
"""Entry point that directly invokes 'cubbi session create'.
This provides a convenient shortcut:
- 'mcx' runs as if you typed 'mc session create'
- 'mcx .' mounts the current directory
- 'mcx /path/to/project' mounts the specified directory
- 'mcx repo-url' clones the repository
- 'cubbix' runs as if you typed 'cubbi session create'
- 'cubbix .' mounts the current directory
- 'cubbix /path/to/project' mounts the specified directory
- 'cubbix repo-url' clones the repository
All command-line options are passed through to 'session create'.
"""
import sys
# Save the program name (e.g., 'mcx')
# Save the program name (e.g., 'cubbix')
prog_name = sys.argv[0]
# Insert 'session' and 'create' commands before any other arguments
sys.argv.insert(1, "session")

174
cubbi/config.py Normal file
View File

@@ -0,0 +1,174 @@
from pathlib import Path
from typing import Dict, Optional
import yaml
from .models import Config, Image
DEFAULT_CONFIG_DIR = Path.home() / ".config" / "cubbi"
DEFAULT_CONFIG_FILE = DEFAULT_CONFIG_DIR / "config.yaml"
DEFAULT_IMAGES_DIR = Path.home() / ".config" / "cubbi" / "images"
PROJECT_ROOT = Path(__file__).parent.parent
BUILTIN_IMAGES_DIR = Path(__file__).parent / "images"
# Dynamically loaded from images directory at runtime
DEFAULT_IMAGES = {}
class ConfigManager:
def __init__(self, config_path: Optional[Path] = None):
self.config_path = config_path or DEFAULT_CONFIG_FILE
self.config_dir = self.config_path.parent
self.images_dir = DEFAULT_IMAGES_DIR
self.config = self._load_or_create_config()
# Always load package images on initialization
# These are separate from the user config
self.builtin_images = self._load_package_images()
def _load_or_create_config(self) -> Config:
"""Load existing config or create a new one with defaults"""
if self.config_path.exists():
try:
with open(self.config_path, "r") as f:
config_data = yaml.safe_load(f) or {}
# Create a new config from scratch, then update with data from file
config = Config(
docker=config_data.get("docker", {}),
defaults=config_data.get("defaults", {}),
)
# Add images
if "images" in config_data:
for image_name, image_data in config_data["images"].items():
config.images[image_name] = Image.model_validate(image_data)
return config
except Exception as e:
print(f"Error loading config: {e}")
return self._create_default_config()
else:
return self._create_default_config()
def _create_default_config(self) -> Config:
"""Create a default configuration"""
self.config_dir.mkdir(parents=True, exist_ok=True)
self.images_dir.mkdir(parents=True, exist_ok=True)
# Initial config without images
config = Config(
docker={
"socket": "/var/run/docker.sock",
"network": "cubbi-network",
},
defaults={
"image": "goose",
},
)
self.save_config(config)
return config
def save_config(self, config: Optional[Config] = None) -> None:
"""Save the current config to disk"""
if config:
self.config = config
self.config_dir.mkdir(parents=True, exist_ok=True)
# Use model_dump with mode="json" for proper serialization of enums
config_dict = self.config.model_dump(mode="json")
# Write to file
with open(self.config_path, "w") as f:
yaml.dump(config_dict, f)
def get_image(self, name: str) -> Optional[Image]:
"""Get an image by name, checking builtin images first, then user-configured ones"""
# Check builtin images first (package images take precedence)
if name in self.builtin_images:
return self.builtin_images[name]
# If not found, check user-configured images
return self.config.images.get(name)
def list_images(self) -> Dict[str, Image]:
"""List all available images (both builtin and user-configured)"""
# Start with user config images
all_images = dict(self.config.images)
# Add builtin images, overriding any user images with the same name
# This ensures that package-provided images always take precedence
all_images.update(self.builtin_images)
return all_images
# Session management has been moved to SessionManager in session.py
def load_image_from_dir(self, image_dir: Path) -> Optional[Image]:
"""Load an image configuration from a directory"""
# Check for image config file
yaml_path = image_dir / "cubbi-image.yaml"
if not yaml_path.exists():
return None
try:
with open(yaml_path, "r") as f:
image_data = yaml.safe_load(f)
# Extract required fields
if not all(
k in image_data
for k in ["name", "description", "version", "maintainer"]
):
print(f"Image config {yaml_path} missing required fields")
return None
# Use Image.model_validate to handle all fields from YAML
# This will map all fields according to the Image model structure
try:
# Ensure image field is set if not in YAML
if "image" not in image_data:
image_data["image"] = f"monadical/cubbi-{image_data['name']}:latest"
image = Image.model_validate(image_data)
return image
except Exception as validation_error:
print(
f"Error validating image data from {yaml_path}: {validation_error}"
)
return None
except Exception as e:
print(f"Error loading image from {yaml_path}: {e}")
return None
def _load_package_images(self) -> Dict[str, Image]:
"""Load all package images from the cubbi/images directory"""
images = {}
if not BUILTIN_IMAGES_DIR.exists():
return images
# Search for cubbi-image.yaml files in each subdirectory
for image_dir in BUILTIN_IMAGES_DIR.iterdir():
if image_dir.is_dir():
image = self.load_image_from_dir(image_dir)
if image:
images[image.name] = image
return images
def get_image_path(self, image_name: str) -> Optional[Path]:
"""Get the directory path for an image"""
# Check package images first (these are the bundled ones)
package_path = BUILTIN_IMAGES_DIR / image_name
if package_path.exists() and package_path.is_dir():
return package_path
# Then check user images
user_path = self.images_dir / image_name
if user_path.exists() and user_path.is_dir():
return user_path
return None

View File

@@ -1,18 +1,19 @@
import concurrent.futures
import hashlib
import logging
import os
import pathlib
import sys
import uuid
import docker
import hashlib
import pathlib
import concurrent.futures
import logging
from typing import Dict, List, Optional, Tuple
import docker
from docker.errors import DockerException, ImageNotFound
from .models import Session, SessionStatus
from .config import ConfigManager
from .session import SessionManager
from .mcp import MCPManager
from .models import Session, SessionStatus
from .session import SessionManager
from .user_config import UserConfigManager
# Configure logging
@@ -41,8 +42,8 @@ class ContainerManager:
sys.exit(1)
def _ensure_network(self) -> None:
"""Ensure the MC network exists"""
network_name = self.config_manager.config.docker.get("network", "mc-network")
"""Ensure the Cubbi network exists"""
network_name = self.config_manager.config.docker.get("network", "cubbi-network")
networks = self.client.networks.list(names=[network_name])
if not networks:
self.client.networks.create(network_name, driver="bridge")
@@ -63,8 +64,8 @@ class ContainerManager:
Returns:
Path to the project configuration directory, or None if no project_name is provided
"""
# Get home directory for the MC config
mc_home = pathlib.Path.home() / ".mc"
# Get home directory for the Cubbi config
cubbi_home = pathlib.Path.home() / ".cubbi"
# Only use project_name if explicitly provided
if project_name:
@@ -72,7 +73,7 @@ class ContainerManager:
project_hash = hashlib.md5(project_name.encode()).hexdigest()
# Create the project config directory path
config_path = mc_home / "projects" / project_hash / "config"
config_path = cubbi_home / "projects" / project_hash / "config"
# Create the directory if it doesn't exist
config_path.parent.mkdir(parents=True, exist_ok=True)
@@ -81,22 +82,22 @@ class ContainerManager:
return config_path
else:
# If no project_name is provided, don't create any config directory
# This ensures we don't mount the /mc-config volume for project-less sessions
# This ensures we don't mount the /cubbi-config volume for project-less sessions
return None
def list_sessions(self) -> List[Session]:
"""List all active MC sessions"""
"""List all active Cubbi sessions"""
sessions = []
try:
containers = self.client.containers.list(
all=True, filters={"label": "mc.session"}
all=True, filters={"label": "cubbi.session"}
)
for container in containers:
container_id = container.id
labels = container.labels
session_id = labels.get("mc.session.id")
session_id = labels.get("cubbi.session.id")
if not session_id:
continue
@@ -108,8 +109,8 @@ class ContainerManager:
session = Session(
id=session_id,
name=labels.get("mc.session.name", f"mc-{session_id}"),
driver=labels.get("mc.driver", "unknown"),
name=labels.get("cubbi.session.name", f"cubbi-{session_id}"),
image=labels.get("cubbi.image", "unknown"),
status=status,
container_id=container_id,
)
@@ -136,7 +137,7 @@ class ContainerManager:
def create_session(
self,
driver_name: str,
image_name: str,
project: Optional[str] = None,
project_name: Optional[str] = None,
environment: Optional[Dict[str, str]] = None,
@@ -146,16 +147,17 @@ class ContainerManager:
networks: Optional[List[str]] = None,
mcp: Optional[List[str]] = None,
run_command: Optional[str] = None,
no_shell: bool = False,
uid: Optional[int] = None,
gid: Optional[int] = None,
model: Optional[str] = None,
provider: Optional[str] = None,
ssh: bool = False,
) -> Optional[Session]:
"""Create a new MC session
"""Create a new Cubbi session
Args:
driver_name: The name of the driver to use
image_name: The name of the image to use
project: Optional project repository URL or local directory path
project_name: Optional explicit project name for configuration persistence
environment: Optional environment variables
@@ -163,23 +165,26 @@ class ContainerManager:
mount_local: Whether to mount the specified local directory to /app (ignored if project is None)
volumes: Optional additional volumes to mount (dict of {host_path: {"bind": container_path, "mode": mode}})
run_command: Optional command to execute before starting the shell
no_shell: Whether to close the container after run_command completes (requires run_command)
networks: Optional list of additional Docker networks to connect to
mcp: Optional list of MCP server names to attach to the session
uid: Optional user ID for the container process
gid: Optional group ID for the container process
model: Optional model to use
provider: Optional provider to use
ssh: Whether to start the SSH server in the container (default: False)
"""
try:
# Validate driver exists
driver = self.config_manager.get_driver(driver_name)
if not driver:
print(f"Driver '{driver_name}' not found")
# Validate image exists
image = self.config_manager.get_image(image_name)
if not image:
print(f"Image '{image_name}' not found")
return None
# Generate session ID and name
session_id = self._generate_session_id()
if not session_name:
session_name = f"mc-{session_id}"
session_name = f"cubbi-{session_id}"
# Ensure network exists
self._ensure_network()
@@ -187,12 +192,12 @@ class ContainerManager:
# Prepare environment variables
env_vars = environment or {}
# Add MC_USER_ID and MC_GROUP_ID for entrypoint script
env_vars["MC_USER_ID"] = str(uid) if uid is not None else "1000"
env_vars["MC_GROUP_ID"] = str(gid) if gid is not None else "1000"
# Add CUBBI_USER_ID and CUBBI_GROUP_ID for entrypoint script
env_vars["CUBBI_USER_ID"] = str(uid) if uid is not None else "1000"
env_vars["CUBBI_GROUP_ID"] = str(gid) if gid is not None else "1000"
# Set SSH environment variable
env_vars["MC_SSH_ENABLED"] = "true" if ssh else "false"
env_vars["CUBBI_SSH_ENABLED"] = "true" if ssh else "false"
# Pass API keys from host environment to container for local development
api_keys = [
@@ -210,10 +215,10 @@ class ContainerManager:
# Pull image if needed
try:
self.client.images.get(driver.image)
self.client.images.get(image.image)
except ImageNotFound:
print(f"Pulling image {driver.image}...")
self.client.images.pull(driver.image)
print(f"Pulling image {image.image}...")
self.client.images.pull(image.image)
# Set up volume mounts
session_volumes = {}
@@ -239,7 +244,7 @@ class ContainerManager:
# Clear project for container environment since we're mounting
project = None
elif is_git_repo:
env_vars["MC_PROJECT_URL"] = project
env_vars["CUBBI_PROJECT_URL"] = project
print(
f"Git repository URL provided - container will clone {project} into /app during initialization"
)
@@ -268,22 +273,22 @@ class ContainerManager:
# Mount the project configuration directory
session_volumes[str(project_config_path)] = {
"bind": "/mc-config",
"bind": "/cubbi-config",
"mode": "rw",
}
# Add environment variables for config path
env_vars["MC_CONFIG_DIR"] = "/mc-config"
env_vars["MC_DRIVER_CONFIG_DIR"] = f"/mc-config/{driver_name}"
env_vars["CUBBI_CONFIG_DIR"] = "/cubbi-config"
env_vars["CUBBI_IMAGE_CONFIG_DIR"] = f"/cubbi-config/{image_name}"
# Create driver-specific config directories and set up direct volume mounts
if driver.persistent_configs:
# Create image-specific config directories and set up direct volume mounts
if image.persistent_configs:
persistent_links_data = [] # To store "source:target" pairs for symlinks
print("Setting up persistent configuration directories:")
for config in driver.persistent_configs:
for config in image.persistent_configs:
# Get target directory path on host
target_dir = project_config_path / config.target.removeprefix(
"/mc-config/"
"/cubbi-config/"
)
# Create directory if it's a directory type config
@@ -298,7 +303,7 @@ class ContainerManager:
# File will be created by the container if needed
# Store the source and target paths for the init script
# Note: config.target is the path *within* /mc-config
# Note: config.target is the path *within* /cubbi-config
persistent_links_data.append(f"{config.source}:{config.target}")
print(
@@ -307,20 +312,20 @@ class ContainerManager:
# Set up persistent links
if persistent_links_data:
env_vars["MC_PERSISTENT_LINKS"] = ";".join(
env_vars["CUBBI_PERSISTENT_LINKS"] = ";".join(
persistent_links_data
)
print(
f"Setting MC_PERSISTENT_LINKS={env_vars['MC_PERSISTENT_LINKS']}"
f"Setting CUBBI_PERSISTENT_LINKS={env_vars['CUBBI_PERSISTENT_LINKS']}"
)
else:
print(
"No project_name provided - skipping configuration directory setup."
)
# Default MC network
# Default Cubbi network
default_network = self.config_manager.config.docker.get(
"network", "mc-network"
"network", "cubbi-network"
)
# Get network list
@@ -439,9 +444,9 @@ class ContainerManager:
env_vars["MCP_NAMES"] = json.dumps(mcp_names)
# Add user-specified networks
# Default MC network
# Default Cubbi network
default_network = self.config_manager.config.docker.get(
"network", "mc-network"
"network", "cubbi-network"
)
# Get network list, ensuring default is first and no duplicates
@@ -467,12 +472,20 @@ class ContainerManager:
target_shell = "/bin/bash"
if run_command:
# Set environment variable for mc-init.sh to pick up
env_vars["MC_RUN_COMMAND"] = run_command
# Set the container's command to be the final shell
# Set environment variable for cubbi-init.sh to pick up
env_vars["CUBBI_RUN_COMMAND"] = run_command
# If no_shell is true, set CUBBI_NO_SHELL environment variable
if no_shell:
env_vars["CUBBI_NO_SHELL"] = "true"
logger.info(
"Setting CUBBI_NO_SHELL=true, container will exit after run command"
)
# Set the container's command to be the final shell (or exit if no_shell is true)
container_command = [target_shell]
logger.info(
f"Setting MC_RUN_COMMAND and targeting shell {target_shell}"
f"Setting CUBBI_RUN_COMMAND and targeting shell {target_shell}"
)
else:
# Use default behavior (often defined by image's ENTRYPOINT/CMD)
@@ -485,16 +498,16 @@ class ContainerManager:
)
# Set default model/provider from user config if not explicitly provided
env_vars["MC_MODEL"] = model or self.user_config_manager.get(
env_vars["CUBBI_MODEL"] = model or self.user_config_manager.get(
"defaults.model", ""
)
env_vars["MC_PROVIDER"] = provider or self.user_config_manager.get(
env_vars["CUBBI_PROVIDER"] = provider or self.user_config_manager.get(
"defaults.provider", ""
)
# Create container
container = self.client.containers.create(
image=driver.image,
image=image.image,
name=session_name,
hostname=session_name,
detach=True,
@@ -503,18 +516,18 @@ class ContainerManager:
environment=env_vars,
volumes=session_volumes,
labels={
"mc.session": "true",
"mc.session.id": session_id,
"mc.session.name": session_name,
"mc.driver": driver_name,
"mc.project": project or "",
"mc.project_name": project_name or "",
"mc.mcps": ",".join(mcp_names) if mcp_names else "",
"cubbi.session": "true",
"cubbi.session.id": session_id,
"cubbi.session.name": session_name,
"cubbi.image": image_name,
"cubbi.project": project or "",
"cubbi.project_name": project_name or "",
"cubbi.mcps": ",".join(mcp_names) if mcp_names else "",
},
network=network_list[0], # Connect to the first network initially
command=container_command, # Set the command
entrypoint=entrypoint, # Set the entrypoint (might be None)
ports={f"{port}/tcp": None for port in driver.ports},
ports={f"{port}/tcp": None for port in image.ports},
)
# Start container
@@ -548,7 +561,7 @@ class ContainerManager:
for mcp_name in mcp_names:
try:
# Get the dedicated network for this MCP
dedicated_network_name = f"mc-mcp-{mcp_name}-network"
dedicated_network_name = f"cubbi-mcp-{mcp_name}-network"
try:
network = self.client.networks.get(dedicated_network_name)
@@ -613,7 +626,7 @@ class ContainerManager:
session = Session(
id=session_id,
name=session_name,
driver=driver_name,
image=image_name,
status=SessionStatus.RUNNING,
container_id=container.id,
ports=ports,
@@ -632,7 +645,7 @@ class ContainerManager:
return None
def close_session(self, session_id: str) -> bool:
"""Close a MC session"""
"""Close a Cubbi session"""
try:
sessions = self.list_sessions()
for session in sessions:
@@ -647,7 +660,7 @@ class ContainerManager:
return False
def connect_session(self, session_id: str) -> bool:
"""Connect to a running MC session"""
"""Connect to a running Cubbi session"""
# Retrieve full session data which should include uid/gid
session_data = self.session_manager.get_session(session_id)
@@ -737,7 +750,7 @@ class ContainerManager:
return False
def close_all_sessions(self, progress_callback=None) -> Tuple[int, bool]:
"""Close all MC sessions with parallel processing and progress reporting
"""Close all Cubbi sessions with parallel processing and progress reporting
Args:
progress_callback: Optional callback function to report progress
@@ -810,15 +823,56 @@ class ContainerManager:
return 0, False
def get_session_logs(self, session_id: str, follow: bool = False) -> Optional[str]:
"""Get logs from a MC session"""
"""Get logs from a Cubbi session"""
try:
sessions = self.list_sessions()
for session in sessions:
if session.id == session_id and session.container_id:
container = self.client.containers.get(session.container_id)
if follow:
for line in container.logs(stream=True, follow=True):
print(line.decode().strip())
# For streamed logs, we'll buffer by line to avoid character-by-character output
import io
from typing import Iterator
def process_log_stream(
stream: Iterator[bytes],
) -> Iterator[str]:
buffer = io.StringIO()
for chunk in stream:
chunk_str = chunk.decode("utf-8", errors="replace")
buffer.write(chunk_str)
# Process complete lines
while True:
line = buffer.getvalue()
newline_pos = line.find("\n")
if newline_pos == -1:
break
# Extract complete line and yield it
complete_line = line[:newline_pos].rstrip()
yield complete_line
# Update buffer to contain only the remaining content
new_buffer = io.StringIO()
new_buffer.write(line[newline_pos + 1 :])
buffer = new_buffer
# Don't forget to yield any remaining content at the end
final_content = buffer.getvalue().strip()
if final_content:
yield final_content
try:
# Process the log stream line by line
for line in process_log_stream(
container.logs(stream=True, follow=True)
):
print(line)
except KeyboardInterrupt:
# Handle Ctrl+C gracefully
print("\nStopped following logs.")
return None
else:
return container.logs().decode()
@@ -831,7 +885,7 @@ class ContainerManager:
return None
def get_init_logs(self, session_id: str, follow: bool = False) -> Optional[str]:
"""Get initialization logs from a MC session
"""Get initialization logs from a Cubbi session
Args:
session_id: The session ID
@@ -861,9 +915,52 @@ class ContainerManager:
f"Following initialization logs for session {session_id}..."
)
print("Press Ctrl+C to stop following")
container.exec_run(
"tail -f /init.log", stream=True, demux=True, tty=True
)
import io
def process_exec_stream(stream):
buffer = io.StringIO()
for chunk_type, chunk_bytes in stream:
if chunk_type != 1: # Skip stderr (type 2)
continue
chunk_str = chunk_bytes.decode(
"utf-8", errors="replace"
)
buffer.write(chunk_str)
# Process complete lines
while True:
line = buffer.getvalue()
newline_pos = line.find("\n")
if newline_pos == -1:
break
# Extract complete line and yield it
complete_line = line[:newline_pos].rstrip()
yield complete_line
# Update buffer to contain only the remaining content
new_buffer = io.StringIO()
new_buffer.write(line[newline_pos + 1 :])
buffer = new_buffer
# Don't forget to yield any remaining content at the end
final_content = buffer.getvalue().strip()
if final_content:
yield final_content
try:
exec_result = container.exec_run(
"tail -f /init.log", stream=True, demux=True
)
# Process the exec stream line by line
for line in process_exec_stream(exec_result[1]):
print(line)
except KeyboardInterrupt:
print("\nStopped following logs.")
return None
else:
exit_code, output = container.exec_run("cat /init.log")

3
cubbi/images/__init__.py Normal file
View File

@@ -0,0 +1,3 @@
"""
MAI container image management
"""

28
cubbi/images/base.py Normal file
View File

@@ -0,0 +1,28 @@
"""
Base image implementation for MAI
"""
from typing import Dict, Optional
from ..models import Image
class ImageManager:
"""Manager for MAI images"""
@staticmethod
def get_default_images() -> Dict[str, Image]:
"""Get the default built-in images"""
from ..config import DEFAULT_IMAGES
return DEFAULT_IMAGES
@staticmethod
def get_image_metadata(image_name: str) -> Optional[Dict]:
"""Get metadata for a specific image"""
from ..config import DEFAULT_IMAGES
if image_name in DEFAULT_IMAGES:
return DEFAULT_IMAGES[image_name].model_dump()
return None

View File

@@ -1,7 +1,7 @@
FROM python:3.12-slim
LABEL maintainer="team@monadical.com"
LABEL description="Goose with MCP servers"
LABEL description="Goose with MCP servers for Cubbi"
# Install system dependencies including gosu for user switching and shadow for useradd/groupadd
RUN apt-get update && apt-get install -y --no-install-recommends \
@@ -41,16 +41,16 @@ RUN curl -fsSL https://github.com/block/goose/releases/download/stable/download_
WORKDIR /app
# Copy initialization scripts
COPY mc-init.sh /mc-init.sh
COPY cubbi-init.sh /cubbi-init.sh
COPY entrypoint.sh /entrypoint.sh
COPY mc-driver.yaml /mc-driver.yaml
COPY cubbi-image.yaml /cubbi-image.yaml
COPY init-status.sh /init-status.sh
COPY update-goose-config.py /usr/local/bin/update-goose-config.py
# Extend env via bashrc
# Make scripts executable
RUN chmod +x /mc-init.sh /entrypoint.sh /init-status.sh \
RUN chmod +x /cubbi-init.sh /entrypoint.sh /init-status.sh \
/usr/local/bin/update-goose-config.py
# Set up initialization status check on login
@@ -59,7 +59,7 @@ RUN echo '[ -x /init-status.sh ] && /init-status.sh' >> /etc/bash.bashrc
# Set up environment
ENV PYTHONUNBUFFERED=1
ENV PYTHONDONTWRITEBYTECODE=1
# Set WORKDIR to /app, common practice and expected by mc-init.sh
# Set WORKDIR to /app, common practice and expected by cubbi-init.sh
WORKDIR /app
# Expose ports

View File

@@ -1,6 +1,6 @@
# Goose Driver for MC
# Goose Image for MC
This driver provides a containerized environment for running [Goose](https://goose.ai).
This image provides a containerized environment for running [Goose](https://goose.ai).
## Features
@@ -17,25 +17,25 @@ This driver provides a containerized environment for running [Goose](https://goo
| `LANGFUSE_INIT_PROJECT_PUBLIC_KEY` | Langfuse public key | No |
| `LANGFUSE_INIT_PROJECT_SECRET_KEY` | Langfuse secret key | No |
| `LANGFUSE_URL` | Langfuse API URL | No |
| `MC_PROJECT_URL` | Project repository URL | No |
| `MC_GIT_SSH_KEY` | SSH key for Git authentication | No |
| `MC_GIT_TOKEN` | Token for Git authentication | No |
| `CUBBI_PROJECT_URL` | Project repository URL | No |
| `CUBBI_GIT_SSH_KEY` | SSH key for Git authentication | No |
| `CUBBI_GIT_TOKEN` | Token for Git authentication | No |
## Build
To build this driver:
To build this image:
```bash
cd drivers/goose
docker build -t monadical/mc-goose:latest .
docker build -t monadical/cubbi-goose:latest .
```
## Usage
```bash
# Create a new session with this driver
mc session create --driver goose
# Create a new session with this image
cubbi session create --driver goose
# Create with project repository
mc session create --driver goose --project github.com/username/repo
cubbi session create --driver goose --project github.com/username/repo
```

View File

@@ -2,10 +2,10 @@ name: goose
description: Goose AI environment
version: 1.0.0
maintainer: team@monadical.com
image: monadical/mc-goose:latest
image: monadical/cubbi-goose:latest
init:
pre_command: /mc-init.sh
pre_command: /cubbi-init.sh
command: /entrypoint.sh
environment:
@@ -25,21 +25,21 @@ environment:
default: https://cloud.langfuse.com
# Project environment variables
- name: MC_PROJECT_URL
- name: CUBBI_PROJECT_URL
description: Project repository URL
required: false
- name: MC_PROJECT_TYPE
- name: CUBBI_PROJECT_TYPE
description: Project repository type (git, svn, etc.)
required: false
default: git
- name: MC_GIT_SSH_KEY
- name: CUBBI_GIT_SSH_KEY
description: SSH key for Git authentication
required: false
sensitive: true
- name: MC_GIT_TOKEN
- name: CUBBI_GIT_TOKEN
description: Token for Git authentication
required: false
sensitive: true
@@ -54,10 +54,10 @@ volumes:
persistent_configs:
- source: "/app/.goose"
target: "/mc-config/goose-app"
target: "/cubbi-config/goose-app"
type: "directory"
description: "Goose memory"
- source: "/home/mcuser/.config/goose"
target: "/mc-config/goose-config"
- source: "/home/cubbi/.config/goose"
target: "/cubbi-config/goose-config"
type: "directory"
description: "Goose configuration"

188
cubbi/images/goose/cubbi-init.sh Executable file
View File

@@ -0,0 +1,188 @@
#!/bin/bash
# Standardized initialization script for Cubbi images
# Redirect all output to both stdout and the log file
exec > >(tee -a /init.log) 2>&1
# Mark initialization as started
echo "=== Cubbi Initialization started at $(date) ==="
# --- START INSERTED BLOCK ---
# Default UID/GID if not provided (should be passed by cubbi tool)
CUBBI_USER_ID=${CUBBI_USER_ID:-1000}
CUBBI_GROUP_ID=${CUBBI_GROUP_ID:-1000}
echo "Using UID: $CUBBI_USER_ID, GID: $CUBBI_GROUP_ID"
# Create group if it doesn't exist
if ! getent group cubbi > /dev/null; then
groupadd -g $CUBBI_GROUP_ID cubbi
else
# If group exists but has different GID, modify it
EXISTING_GID=$(getent group cubbi | cut -d: -f3)
if [ "$EXISTING_GID" != "$CUBBI_GROUP_ID" ]; then
groupmod -g $CUBBI_GROUP_ID cubbi
fi
fi
# Create user if it doesn't exist
if ! getent passwd cubbi > /dev/null; then
useradd --shell /bin/bash --uid $CUBBI_USER_ID --gid $CUBBI_GROUP_ID --no-create-home cubbi
else
# If user exists but has different UID/GID, modify it
EXISTING_UID=$(getent passwd cubbi | cut -d: -f3)
EXISTING_GID=$(getent passwd cubbi | cut -d: -f4)
if [ "$EXISTING_UID" != "$CUBBI_USER_ID" ] || [ "$EXISTING_GID" != "$CUBBI_GROUP_ID" ]; then
usermod --uid $CUBBI_USER_ID --gid $CUBBI_GROUP_ID cubbi
fi
fi
# Create home directory and set permissions
mkdir -p /home/cubbi
chown $CUBBI_USER_ID:$CUBBI_GROUP_ID /home/cubbi
mkdir -p /app
chown $CUBBI_USER_ID:$CUBBI_GROUP_ID /app
# Copy /root/.local/bin to the user's home directory
if [ -d /root/.local/bin ]; then
echo "Copying /root/.local/bin to /home/cubbi/.local/bin..."
mkdir -p /home/cubbi/.local/bin
cp -r /root/.local/bin/* /home/cubbi/.local/bin/
chown -R $CUBBI_USER_ID:$CUBBI_GROUP_ID /home/cubbi/.local
fi
# Start SSH server only if explicitly enabled
if [ "$CUBBI_SSH_ENABLED" = "true" ]; then
echo "Starting SSH server..."
/usr/sbin/sshd
else
echo "SSH server disabled (use --ssh flag to enable)"
fi
# --- END INSERTED BLOCK ---
echo "INIT_COMPLETE=false" > /init.status
# Project initialization
if [ -n "$CUBBI_PROJECT_URL" ]; then
echo "Initializing project: $CUBBI_PROJECT_URL"
# Set up SSH key if provided
if [ -n "$CUBBI_GIT_SSH_KEY" ]; then
mkdir -p ~/.ssh
echo "$CUBBI_GIT_SSH_KEY" > ~/.ssh/id_ed25519
chmod 600 ~/.ssh/id_ed25519
ssh-keyscan github.com >> ~/.ssh/known_hosts 2>/dev/null
ssh-keyscan gitlab.com >> ~/.ssh/known_hosts 2>/dev/null
ssh-keyscan bitbucket.org >> ~/.ssh/known_hosts 2>/dev/null
fi
# Set up token if provided
if [ -n "$CUBBI_GIT_TOKEN" ]; then
git config --global credential.helper store
echo "https://$CUBBI_GIT_TOKEN:x-oauth-basic@github.com" > ~/.git-credentials
fi
# Clone repository
git clone $CUBBI_PROJECT_URL /app
cd /app
# Run project-specific initialization if present
if [ -f "/app/.cubbi/init.sh" ]; then
bash /app/.cubbi/init.sh
fi
# Persistent configs are now directly mounted as volumes
# No need to create symlinks anymore
if [ -n "$CUBBI_CONFIG_DIR" ] && [ -d "$CUBBI_CONFIG_DIR" ]; then
echo "Using persistent configuration volumes (direct mounts)"
fi
fi
# Goose uses self-hosted instance, no API key required
# Set up Langfuse logging if credentials are provided
if [ -n "$LANGFUSE_INIT_PROJECT_SECRET_KEY" ] && [ -n "$LANGFUSE_INIT_PROJECT_PUBLIC_KEY" ]; then
echo "Setting up Langfuse logging"
export LANGFUSE_INIT_PROJECT_SECRET_KEY="$LANGFUSE_INIT_PROJECT_SECRET_KEY"
export LANGFUSE_INIT_PROJECT_PUBLIC_KEY="$LANGFUSE_INIT_PROJECT_PUBLIC_KEY"
export LANGFUSE_URL="${LANGFUSE_URL:-https://cloud.langfuse.com}"
fi
# Ensure /cubbi-config directory exists (required for symlinks)
if [ ! -d "/cubbi-config" ]; then
echo "Creating /cubbi-config directory since it doesn't exist"
mkdir -p /cubbi-config
chown $CUBBI_USER_ID:$CUBBI_GROUP_ID /cubbi-config
fi
# Create symlinks for persistent configurations defined in the image
if [ -n "$CUBBI_PERSISTENT_LINKS" ]; then
echo "Creating persistent configuration symlinks..."
# Split by semicolon
IFS=';' read -ra LINKS <<< "$CUBBI_PERSISTENT_LINKS"
for link_pair in "${LINKS[@]}"; do
# Split by colon
IFS=':' read -r source_path target_path <<< "$link_pair"
if [ -z "$source_path" ] || [ -z "$target_path" ]; then
echo "Warning: Invalid link pair format '$link_pair', skipping."
continue
fi
echo "Processing link: $source_path -> $target_path"
parent_dir=$(dirname "$source_path")
# Ensure parent directory of the link source exists and is owned by cubbi
if [ ! -d "$parent_dir" ]; then
echo "Creating parent directory: $parent_dir"
mkdir -p "$parent_dir"
echo "Changing ownership of parent $parent_dir to $CUBBI_USER_ID:$CUBBI_GROUP_ID"
chown "$CUBBI_USER_ID:$CUBBI_GROUP_ID" "$parent_dir" || echo "Warning: Could not chown parent $parent_dir"
fi
# Create the symlink (force, no-dereference)
echo "Creating symlink: ln -sfn $target_path $source_path"
ln -sfn "$target_path" "$source_path"
# Optionally, change ownership of the symlink itself
echo "Changing ownership of symlink $source_path to $CUBBI_USER_ID:$CUBBI_GROUP_ID"
chown -h "$CUBBI_USER_ID:$CUBBI_GROUP_ID" "$source_path" || echo "Warning: Could not chown symlink $source_path"
done
echo "Persistent configuration symlinks created."
fi
# Update Goose configuration with available MCP servers (run as cubbi after symlinks are created)
if [ -f "/usr/local/bin/update-goose-config.py" ]; then
echo "Updating Goose configuration with MCP servers as cubbi..."
gosu cubbi /usr/local/bin/update-goose-config.py
elif [ -f "$(dirname "$0")/update-goose-config.py" ]; then
echo "Updating Goose configuration with MCP servers as cubbi..."
gosu cubbi "$(dirname "$0")/update-goose-config.py"
else
echo "Warning: update-goose-config.py script not found. Goose configuration will not be updated."
fi
# Run the user command first, if set, as cubbi
if [ -n "$CUBBI_RUN_COMMAND" ]; then
echo "--- Executing initial command: $CUBBI_RUN_COMMAND ---";
gosu cubbi sh -c "$CUBBI_RUN_COMMAND"; # Run user command as cubbi
COMMAND_EXIT_CODE=$?;
echo "--- Initial command finished (exit code: $COMMAND_EXIT_CODE) ---";
# If CUBBI_NO_SHELL is set, exit instead of starting a shell
if [ "$CUBBI_NO_SHELL" = "true" ]; then
echo "--- CUBBI_NO_SHELL=true, exiting container without starting shell ---";
# Mark initialization as complete before exiting
echo "=== Cubbi Initialization completed at $(date) ==="
echo "INIT_COMPLETE=true" > /init.status
exit $COMMAND_EXIT_CODE;
fi;
fi;
# Mark initialization as complete
echo "=== Cubbi Initialization completed at $(date) ==="
echo "INIT_COMPLETE=true" > /init.status
exec gosu cubbi "$@"

View File

@@ -1,7 +1,7 @@
#!/bin/bash
# Entrypoint script for Goose driver
# Entrypoint script for Goose image
# Executes the standard initialization script, which handles user setup,
# service startup (like sshd), and switching to the non-root user
# before running the container's command (CMD).
exec /mc-init.sh "$@"
exec /cubbi-init.sh "$@"

View File

@@ -28,4 +28,4 @@ if ! grep -q "INIT_COMPLETE=true" "/init.status" 2>/dev/null; then
fi
fi
exec gosu mcuser /bin/bash -il
exec gosu cubbi /bin/bash -il

View File

@@ -39,8 +39,8 @@ def update_config():
}
# Update goose configuration with model and provider from environment variables
goose_model = os.environ.get("MC_MODEL")
goose_provider = os.environ.get("MC_PROVIDER")
goose_model = os.environ.get("CUBBI_MODEL")
goose_provider = os.environ.get("CUBBI_PROVIDER")
if goose_model:
config_data["GOOSE_MODEL"] = goose_model

View File

@@ -1,15 +1,16 @@
"""
MCP (Model Control Protocol) server management for Monadical Container.
MCP (Model Control Protocol) server management for Cubbi Container.
"""
import os
import docker
import logging
import os
import tempfile
from typing import Dict, List, Optional, Any
from typing import Any, Dict, List, Optional
import docker
from docker.errors import DockerException, ImageNotFound, NotFound
from .models import MCPStatus, RemoteMCP, DockerMCP, ProxyMCP, MCPContainer
from .models import DockerMCP, MCPContainer, MCPStatus, ProxyMCP, RemoteMCP
from .user_config import UserConfigManager
# Configure logging
@@ -37,7 +38,7 @@ class MCPManager:
"""Ensure the MCP network exists and return its name.
Note: This is used only by the inspector, not for session-to-MCP connections.
"""
network_name = "mc-mcp-network"
network_name = "cubbi-mcp-network"
if self.client:
networks = self.client.networks.list(names=[network_name])
if not networks:
@@ -53,7 +54,7 @@ class MCPManager:
Returns:
The name of the dedicated network
"""
network_name = f"mc-mcp-{mcp_name}-network"
network_name = f"cubbi-mcp-{mcp_name}-network"
if self.client:
networks = self.client.networks.list(names=[network_name])
if not networks:
@@ -281,7 +282,7 @@ class MCPManager:
def get_mcp_container_name(self, mcp_name: str) -> str:
"""Get the Docker container name for an MCP server."""
return f"mc_mcp_{mcp_name}"
return f"cubbi_mcp_{mcp_name}"
def start_mcp(self, name: str) -> Dict[str, Any]:
"""Start an MCP server container."""
@@ -373,9 +374,9 @@ class MCPManager:
network=None, # Start without network, we'll add it with aliases
environment=mcp_config.get("env", {}),
labels={
"mc.mcp": "true",
"mc.mcp.name": name,
"mc.mcp.type": "docker",
"cubbi.mcp": "true",
"cubbi.mcp.name": name,
"cubbi.mcp.type": "docker",
},
)
@@ -539,7 +540,7 @@ ENTRYPOINT ["/entrypoint.sh"]
f.write(dockerfile_content)
# Build the image
custom_image_name = f"mc_mcp_proxy_{name}"
custom_image_name = f"cubbi_mcp_proxy_{name}"
logger.info(f"Building custom proxy image: {custom_image_name}")
self.client.images.build(
path=tmp_dir,
@@ -576,9 +577,9 @@ ENTRYPOINT ["/entrypoint.sh"]
}
},
labels={
"mc.mcp": "true",
"mc.mcp.name": name,
"mc.mcp.type": "proxy",
"cubbi.mcp": "true",
"cubbi.mcp.name": name,
"cubbi.mcp.type": "proxy",
},
ports=port_bindings, # Bind the SSE port to the host if configured
)
@@ -815,8 +816,10 @@ ENTRYPOINT ["/entrypoint.sh"]
if not self.client:
raise Exception("Docker client is not available")
# Get all containers with the mc.mcp label
containers = self.client.containers.list(all=True, filters={"label": "mc.mcp"})
# Get all containers with the cubbi.mcp label
containers = self.client.containers.list(
all=True, filters={"label": "cubbi.mcp"}
)
result = []
for container in containers:
@@ -857,13 +860,13 @@ ENTRYPOINT ["/entrypoint.sh"]
# Create MCPContainer object
mcp_container = MCPContainer(
name=labels.get("mc.mcp.name", "unknown"),
name=labels.get("cubbi.mcp.name", "unknown"),
container_id=container.id,
status=status,
image=container_info["Config"]["Image"],
ports=ports,
created_at=container_info["Created"],
type=labels.get("mc.mcp.type", "unknown"),
type=labels.get("cubbi.mcp.type", "unknown"),
)
result.append(mcp_container)

View File

@@ -1,5 +1,6 @@
from enum import Enum
from typing import Dict, List, Optional, Union, Any
from typing import Any, Dict, List, Optional, Union
from pydantic import BaseModel, Field
@@ -17,7 +18,7 @@ class MCPStatus(str, Enum):
FAILED = "failed"
class DriverEnvironmentVariable(BaseModel):
class ImageEnvironmentVariable(BaseModel):
name: str
description: str
required: bool = False
@@ -37,19 +38,19 @@ class VolumeMount(BaseModel):
description: str = ""
class DriverInit(BaseModel):
class ImageInit(BaseModel):
pre_command: Optional[str] = None
command: str
class Driver(BaseModel):
class Image(BaseModel):
name: str
description: str
version: str
maintainer: str
image: str
init: Optional[DriverInit] = None
environment: List[DriverEnvironmentVariable] = []
init: Optional[ImageInit] = None
environment: List[ImageEnvironmentVariable] = []
ports: List[int] = []
volumes: List[VolumeMount] = []
persistent_configs: List[PersistentConfig] = []
@@ -97,7 +98,7 @@ class MCPContainer(BaseModel):
class Session(BaseModel):
id: str
name: str
driver: str
image: str
status: SessionStatus
container_id: Optional[str] = None
ports: Dict[int, int] = Field(default_factory=dict)
@@ -105,7 +106,7 @@ class Session(BaseModel):
class Config(BaseModel):
docker: Dict[str, str] = Field(default_factory=dict)
drivers: Dict[str, Driver] = Field(default_factory=dict)
images: Dict[str, Image] = Field(default_factory=dict)
defaults: Dict[str, object] = Field(
default_factory=dict
) # Can store strings, booleans, or other values

View File

@@ -1,13 +1,14 @@
"""
Session storage management for Monadical Container Tool.
Session storage management for Cubbi Container Tool.
"""
import os
import yaml
from pathlib import Path
from typing import Dict, Optional
DEFAULT_SESSIONS_FILE = Path.home() / ".config" / "mc" / "sessions.yaml"
import yaml
DEFAULT_SESSIONS_FILE = Path.home() / ".config" / "cubbi" / "sessions.yaml"
class SessionManager:
@@ -18,7 +19,7 @@ class SessionManager:
Args:
sessions_path: Optional path to the sessions file.
Defaults to ~/.config/mc/sessions.yaml.
Defaults to ~/.config/cubbi/sessions.yaml.
"""
self.sessions_path = sessions_path or DEFAULT_SESSIONS_FILE
self.sessions = self._load_sessions()

View File

@@ -1,11 +1,12 @@
"""
User configuration manager for Monadical Container Tool.
User configuration manager for Cubbi Container Tool.
"""
import os
import yaml
from pathlib import Path
from typing import Any, Dict, Optional, List, Tuple
from typing import Any, Dict, List, Optional, Tuple
import yaml
# Define the environment variable mappings
ENV_MAPPINGS = {
@@ -27,11 +28,11 @@ class UserConfigManager:
Args:
config_path: Optional path to the configuration file.
Defaults to ~/.config/mc/config.yaml.
Defaults to ~/.config/cubbi/config.yaml.
"""
# Default to ~/.config/mc/config.yaml
# Default to ~/.config/cubbi/config.yaml
self.config_path = Path(
config_path or os.path.expanduser("~/.config/mc/config.yaml")
config_path or os.path.expanduser("~/.config/cubbi/config.yaml")
)
self.config = self._load_config()
@@ -89,10 +90,10 @@ class UserConfigManager:
"""Get the default configuration."""
return {
"defaults": {
"driver": "goose",
"image": "goose",
"connect": True,
"mount_local": True,
"networks": [], # Default networks to connect to (besides mc-network)
"networks": [], # Default networks to connect to (besides cubbi-network)
"volumes": [], # Default volumes to mount, format: "source:dest"
"mcps": [], # Default MCP servers to connect to
"model": "claude-3-5-sonnet-latest", # Default LLM model to use
@@ -106,7 +107,7 @@ class UserConfigManager:
"google": {},
},
"docker": {
"network": "mc-network",
"network": "cubbi-network",
},
"ui": {
"colors": True,
@@ -133,7 +134,7 @@ class UserConfigManager:
"""Get a configuration value by dot-notation path.
Args:
key_path: The configuration path (e.g., "defaults.driver")
key_path: The configuration path (e.g., "defaults.image")
default: The default value to return if not found
Returns:
@@ -165,7 +166,7 @@ class UserConfigManager:
"""Set a configuration value by dot-notation path.
Args:
key_path: The configuration path (e.g., "defaults.driver")
key_path: The configuration path (e.g., "defaults.image")
value: The value to set
"""
# Handle shorthand service paths (e.g., "langfuse.url")

View File

@@ -1,22 +1,22 @@
# MC - Monadical AI Container Tool
# Cubbi - Container Tool
## Overview
MC (Monadical Container) is a command-line tool for managing ephemeral
Cubbi is a command-line tool for managing ephemeral
containers that run AI tools and development environments. It works with both
local Docker and a dedicated remote web service that manages containers in a
Docker-in-Docker (DinD) environment.
## Technology Stack
### MC Service
### Cubbi Service
- **Web Framework**: FastAPI for high-performance, async API endpoints
- **Package Management**: uv (Astral) for dependency management
- **Database**: SQLite for development, PostgreSQL for production
- **Container Management**: Docker SDK for Python
- **Authentication**: OAuth 2.0 integration with Authentik
### MC CLI
### Cubbi CLI
- **Language**: Python
- **Package Management**: uv for dependency management
- **Distribution**: Standalone binary via PyInstaller or similar
@@ -26,17 +26,17 @@ Docker-in-Docker (DinD) environment.
### Components
1. **CLI Tool (`mc`)**: The command-line interface users interact with
2. **MC Service**: A web service that handles remote container execution
3. **Container Drivers**: Predefined container templates for various AI tools
1. **CLI Tool (`cubbi`)**: The command-line interface users interact with
2. **Cubbi Service**: A web service that handles remote container execution
3. **Container Images**: Predefined container templates for various AI tools
### Architecture Diagram
```
┌─────────────┐ ┌─────────────────────────┐
│ │ │ │
MC CLI │◄─────────►│ Local Docker Daemon │
│ (mc) │ │ │
│ Cubbi CLI │◄─────────►│ Local Docker Daemon │
│ (cubbi) │ │ │
│ │ └─────────────────────────┘
└──────┬──────┘
@@ -44,8 +44,8 @@ Docker-in-Docker (DinD) environment.
┌──────▼──────┐ ┌─────────────────────────┐
│ │ │ │
MC Service │◄─────────►│ Docker-in-Docker │
(Web API) │ │ │
Cubbi │◄─────────►│ Docker-in-Docker │
Service │ │ │
│ │ └─────────────────────────┘
└─────────────┘
@@ -61,23 +61,23 @@ Docker-in-Docker (DinD) environment.
## Core Concepts
- **Session**: An active container instance with a specific driver
- **Driver**: A predefined container template with specific AI tools installed
- **Remote**: A configured MC service instance
- **Session**: An active container instance with a specific image
- **Image**: A predefined container template with specific AI tools installed
- **Remote**: A configured cubbi service instance
## User Configuration
MC supports user-specific configuration via a YAML file located at `~/.config/mc/config.yaml`. This provides a way to set default values, store service credentials, and customize behavior without modifying code.
Cubbi supports user-specific configuration via a YAML file located at `~/.config/cubbi/config.yaml`. This provides a way to set default values, store service credentials, and customize behavior without modifying code.
### Configuration File Structure
```yaml
# ~/.config/mc/config.yaml
# ~/.config/cubbi/config.yaml
defaults:
driver: "goose" # Default driver to use
image: "goose" # Default image to use
connect: true # Automatically connect after creating session
mount_local: true # Mount local directory by default
networks: [] # Default networks to connect to (besides mc-network)
networks: [] # Default networks to connect to (besides cubbi-network)
services:
# Service credentials with simplified naming
@@ -97,17 +97,17 @@ services:
api_key: "sk-or-..."
docker:
network: "mc-network" # Default Docker network to use
network: "cubbi-network" # Default Docker network to use
socket: "/var/run/docker.sock" # Docker socket path
remote:
default: "production" # Default remote to use
endpoints:
production:
url: "https://mc.monadical.com"
url: "https://cubbi.monadical.com"
auth_method: "oauth"
staging:
url: "https://mc-staging.monadical.com"
url: "https://cubbi-staging.monadical.com"
auth_method: "oauth"
ui:
@@ -145,22 +145,22 @@ The simplified configuration names are mapped to environment variables:
```bash
# View entire configuration
mc config list
cubbi config list
# Get specific configuration value
mc config get defaults.driver
cubbi config get defaults.driver
# Set configuration value (using simplified naming)
mc config set langfuse.url "https://cloud.langfuse.com"
mc config set openai.api_key "sk-..."
cubbi config set langfuse.url "https://cloud.langfuse.com"
cubbi config set openai.api_key "sk-..."
# Network configuration
mc config network list # List default networks
mc config network add example-network # Add a network to defaults
mc config network remove example-network # Remove a network from defaults
cubbi config network list # List default networks
cubbi config network add example-network # Add a network to defaults
cubbi config network remove example-network # Remove a network from defaults
# Reset configuration to defaults
mc config reset
cubbi config reset
```
## CLI Tool Commands
@@ -169,81 +169,81 @@ mc config reset
```bash
# Create a new session locally (shorthand)
mc
cubbi
# List active sessions on local system
mc session list
cubbi session list
# Create a new session locally
mc session create [OPTIONS]
cubbi session create [OPTIONS]
# Create a session with a specific driver
mc session create --driver goose
# Create a session with a specific image
cubbi session create --image goose
# Create a session with a specific project repository
mc session create --driver goose --project github.com/hello/private
cubbi session create --image goose --project github.com/hello/private
# Create a session with external networks
mc session create --network teamnet --network othernetwork
cubbi session create --network teamnet --network othernetwork
# Create a session with a project (shorthand)
mc git@github.com:hello/private
cubbi git@github.com:hello/private
# Close a specific session
mc session close <id>
cubbi session close <id>
# Connect to an existing session
mc session connect <id>
cubbi session connect <id>
```
### Remote Management
```bash
# Add a remote MC service
mc remote add <name> <url>
# Add a remote Cubbi service
cubbi remote add <name> <url>
# List configured remote services
mc remote list
cubbi remote list
# Remove a remote service
mc remote remove <name>
cubbi remote remove <name>
# Authenticate with a remote service
mc -r <remote_name> auth
cubbi -r <remote_name> auth
# Create a session on a remote service
mc -r <remote_name> [session create]
cubbi -r <remote_name> [session create]
# List sessions on a remote service
mc -r <remote_name> session list
cubbi -r <remote_name> session list
```
### Environment Variables
```bash
# Set environment variables for a session
mc session create -e VAR1=value1 -e VAR2=value2
cubbi session create -e VAR1=value1 -e VAR2=value2
# Set environment variables for a remote session
mc -r <remote_name> session create -e VAR1=value1
cubbi -r <remote_name> session create -e VAR1=value1
```
### Logging
```bash
# Stream logs from a session
mc session logs <id>
cubbi session logs <id>
# Stream logs with follow option
mc session logs <id> -f
cubbi session logs <id> -f
```
## MC Service Specification
## Cubbi Service Specification
### Overview
The MC Service is a web service that manages ephemeral containers in a Docker-in-Docker environment. It provides a REST API for container lifecycle management, authentication, and real-time log streaming.
The Cubbi Service is a web service that manages ephemeral containers in a Docker-in-Docker environment. It provides a REST API for container lifecycle management, authentication, and real-time log streaming.
### API Endpoints
@@ -258,19 +258,19 @@ POST /auth/logout - Invalidate current token
### Authentik Integration
The MC Service integrates with Authentik at https://authentik.monadical.io using OAuth 2.0:
The Cubbi Service integrates with Authentik at https://authentik.monadical.io using OAuth 2.0:
1. **Application Registration**:
- MC Service is registered as an OAuth application in Authentik
- Cubbi Service is registered as an OAuth application in Authentik
- Configured with redirect URI to `/auth/callback`
- Assigned appropriate scopes for user identification
2. **Authentication Flow**:
- User initiates authentication via CLI
- MC CLI opens browser to Authentik authorization URL
- Cubbi CLI opens browser to Authentik authorization URL
- User logs in through Authentik's interface
- Authentik redirects to callback URL with authorization code
- MC Service exchanges code for access and refresh tokens
- Cubbi Service exchanges code for access and refresh tokens
- CLI receives and securely stores tokens
3. **Token Management**:
@@ -289,11 +289,11 @@ POST /sessions/{id}/connect - Establish connection to session
GET /sessions/{id}/logs - Stream session logs
```
#### Drivers
#### Images
```
GET /drivers - List available drivers
GET /drivers/{name} - Get driver details
GET /images - List available images
GET /images/{name} - Get image details
```
#### Projects
@@ -309,19 +309,19 @@ DELETE /projects/{id} - Remove a project
### Service Configuration
```yaml
# mc-service.yaml
# cubbi-service.yaml
server:
port: 3000
host: 0.0.0.0
docker:
socket: /var/run/docker.sock
network: mc-network
network: cubbi-network
auth:
provider: authentik
url: https://authentik.monadical.io
clientId: mc-service
clientId: cubbi-service
logging:
providers:
@@ -332,13 +332,13 @@ logging:
public_key: ${LANGFUSE_INIT_PROJECT_PUBLIC_KEY}
secret_key: ${LANGFUSE_INIT_PROJECT_SECRET_KEY}
drivers:
images:
- name: goose
image: monadical/mc-goose:latest
image: monadical/cubbi-goose:latest
- name: aider
image: monadical/mc-aider:latest
image: monadical/cubbi-aider:latest
- name: claude-code
image: monadical/mc-claude-code:latest
image: monadical/cubbi-claude-code:latest
projects:
storage:
@@ -352,7 +352,7 @@ projects:
### Docker-in-Docker Implementation
The MC Service runs in a container with access to the host's Docker socket, allowing it to create and manage sibling containers. This approach provides:
The Cubbi Service runs in a container with access to the host's Docker socket, allowing it to create and manage sibling containers. This approach provides:
1. Isolation between containers
2. Simple lifecycle management
@@ -367,7 +367,7 @@ For remote connections to containers, the service provides two methods:
### Logging Implementation
The MC Service implements log collection and forwarding:
The Cubbi Service implements log collection and forwarding:
1. Container logs are captured using Docker's logging drivers
2. Logs are forwarded to configured providers (Fluentd, Langfuse)
@@ -377,22 +377,22 @@ The MC Service implements log collection and forwarding:
### Persistent Project Configuration
MC provides persistent storage for project-specific configurations that need to survive container restarts. This is implemented through a dedicated volume mount and symlink system:
Cubbi provides persistent storage for project-specific configurations that need to survive container restarts. This is implemented through a dedicated volume mount and symlink system:
1. **Configuration Storage**:
- Each project has a dedicated configuration directory on the host at `~/.mc/projects/<project-hash>/config`
- Each project has a dedicated configuration directory on the host at `~/.cubbi/projects/<project-hash>/config`
- For projects specified by URL, the hash is derived from the repository URL
- For local projects, the hash is derived from the absolute path of the local directory
- This directory is mounted into the container at `/mc-config`
- This directory is mounted into the container at `/cubbi-config`
2. **Driver Configuration**:
- Each driver can specify configuration files/directories that should persist across sessions
- These are defined in the driver's `mc-driver.yaml` file in the `persistent_configs` section
- Example for Goose driver:
2. **Image Configuration**:
- Each image can specify configuration files/directories that should persist across sessions
- These are defined in the image's `cubbi-image.yaml` file in the `persistent_configs` section
- Example for Goose image:
```yaml
persistent_configs:
- source: "/app/.goose" # Path in container
target: "/mc-config/goose" # Path in persistent storage
target: "/cubbi-config/goose" # Path in persistent storage
type: "directory" # directory or file
description: "Goose memory and configuration"
```
@@ -406,8 +406,8 @@ MC provides persistent storage for project-specific configurations that need to
4. **Environment Variables**:
- Container has access to configuration location via environment variables:
```
MC_CONFIG_DIR=/mc-config
MC_DRIVER_CONFIG_DIR=/mc-config/<driver-name>
CUBBI_CONFIG_DIR=/cubbi-config
CUBBI_IMAGE_CONFIG_DIR=/cubbi-config/<image-name>
```
This ensures that important configurations like Goose's memory store, authentication tokens, and other state information persist between container sessions while maintaining isolation between different projects.
@@ -418,21 +418,21 @@ Users can add projects with associated credentials:
```bash
# Add a project with SSH key
mc project add github.com/hello/private --ssh-key ~/.ssh/id_ed25519
cubbi project add github.com/hello/private --ssh-key ~/.ssh/id_ed25519
# Add a project with token authentication
mc project add github.com/hello/private --token ghp_123456789
cubbi project add github.com/hello/private --token ghp_123456789
# List all projects
mc project list
cubbi project list
# Remove a project
mc project remove github.com/hello/private
cubbi project remove github.com/hello/private
```
### Project Configuration
Projects are stored in the MC service and referenced by their repository URL. The configuration includes:
Projects are stored in the Cubbi service and referenced by their repository URL. The configuration includes:
```yaml
# Project configuration
@@ -448,60 +448,59 @@ auth:
public_key: ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI...
```
## Driver Implementation
## Image Implementation
### Driver Structure
### Image Structure
Each driver is a Docker image with a standardized structure:
Each image is a Docker container with a standardized structure:
```
/
├── entrypoint.sh # Container initialization
├── mc-init.sh # Standardized initialization script
├── mc-driver.yaml # Driver metadata and configuration
├── cubbi-init.sh # Standardized initialization script
├── cubbi-image.yaml # Image metadata and configuration
├── tool/ # AI tool installation
└── ssh/ # SSH server configuration
```
### Standardized Initialization Script
All drivers include a standardized `mc-init.sh` script that handles common initialization tasks:
All images include a standardized `cubbi-init.sh` script that handles common initialization tasks:
```bash
#!/bin/bash
# Project initialization
if [ -n "$MC_PROJECT_URL" ]; then
echo "Initializing project: $MC_PROJECT_URL"
if [ -n "$CUBBI_PROJECT_URL" ]; then
echo "Initializing project: $CUBBI_PROJECT_URL"
# Set up SSH key if provided
if [ -n "$MC_GIT_SSH_KEY" ]; then
if [ -n "$CUBBI_GIT_SSH_KEY" ]; then
mkdir -p ~/.ssh
echo "$MC_GIT_SSH_KEY" > ~/.ssh/id_ed25519
echo "$CUBBI_GIT_SSH_KEY" > ~/.ssh/id_ed25519
chmod 600 ~/.ssh/id_ed25519
ssh-keyscan github.com >> ~/.ssh/known_hosts 2>/dev/null
fi
# Set up token if provided
if [ -n "$MC_GIT_TOKEN" ]; then
if [ -n "$CUBBI_GIT_TOKEN" ]; then
git config --global credential.helper store
echo "https://$MC_GIT_TOKEN:x-oauth-basic@github.com" > ~/.git-credentials
echo "https://$CUBBI_GIT_TOKEN:x-oauth-basic@github.com" > ~/.git-credentials
fi
# Clone repository
git clone $MC_PROJECT_URL /app
git clone $CUBBI_PROJECT_URL /app
cd /app
# Run project-specific initialization if present
if [ -f "/app/.mc/init.sh" ]; then
bash /app/.mc/init.sh
if [ -f "/app/.cubbi/init.sh" ]; then
bash /app/.cubbi/init.sh
fi
fi
# Driver-specific initialization continues...
# Image-specific initialization continues...
```
### Driver Configuration (mc-driver.yaml)
### Image Configuration (cubbi-image.yaml)
```yaml
name: goose
@@ -510,7 +509,7 @@ version: 1.0.0
maintainer: team@monadical.com
init:
pre_command: /mc-init.sh
pre_command: /cubbi-init.sh
command: /entrypoint.sh
environment:
@@ -524,21 +523,21 @@ environment:
required: false
# Project environment variables
- name: MC_PROJECT_URL
- name: CUBBI_PROJECT_URL
description: Project repository URL
required: false
- name: MC_PROJECT_TYPE
- name: CUBBI_PROJECT_TYPE
description: Project repository type (git, svn, etc.)
required: false
default: git
- name: MC_GIT_SSH_KEY
- name: CUBBI_GIT_SSH_KEY
description: SSH key for Git authentication
required: false
sensitive: true
- name: MC_GIT_TOKEN
- name: CUBBI_GIT_TOKEN
description: Token for Git authentication
required: false
sensitive: true
@@ -553,12 +552,12 @@ volumes:
persistent_configs:
- source: "/app/.goose"
target: "/mc-config/goose"
target: "/cubbi-config/goose"
type: "directory"
description: "Goose memory and configuration"
```
### Example Built-in Drivers
### Example Built-in images
1. **goose**: Goose with MCP servers
2. **aider**: Aider coding assistant
@@ -569,32 +568,32 @@ persistent_configs:
### Docker Network Integration
MC provides flexible network management for containers:
Cubbi provides flexible network management for containers:
1. **Default MC Network**:
- Each container is automatically connected to the MC network (`mc-network` by default)
1. **Default Cubbi Network**:
- Each container is automatically connected to the Cubbi network (`cubbi-network` by default)
- This ensures containers can communicate with each other
2. **External Network Connection**:
- Containers can be connected to one or more external Docker networks
- This allows integration with existing infrastructure (e.g., databases, web servers)
- Networks can be specified at session creation time: `mc session create --network mynetwork`
- Networks can be specified at session creation time: `cubbi session create --network mynetwork`
3. **Default Networks Configuration**:
- Users can configure default networks in their configuration
- These networks will be used for all new sessions unless overridden
- Managed with `mc config network` commands
- Managed with `cubbi config network` commands
4. **Network Command Examples**:
```bash
# Use with session creation
mc session create --network teamnet
cubbi session create --network teamnet
# Use with multiple networks
mc session create --network teamnet --network dbnet
cubbi session create --network teamnet --network dbnet
# Configure default networks
mc config network add teamnet
cubbi config network add teamnet
```
## Security Considerations
@@ -607,15 +606,15 @@ MC provides flexible network management for containers:
## Deployment
### MC Service Deployment
### Cubbi Service Deployment
```yaml
# docker-compose.yml for MC Service
# docker-compose.yml for Cubbi Service
version: '3.8'
services:
mc-service:
image: monadical/mc-service:latest
cubbi-service:
image: monadical/cubbi-service:latest
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- ./config:/app/config
@@ -625,10 +624,10 @@ services:
- AUTH_URL=https://authentik.monadical.io
- LANGFUSE_API_KEY=your_api_key
networks:
- mc-network
- cubbi-network
networks:
mc-network:
cubbi-network:
driver: bridge
```
@@ -638,33 +637,33 @@ networks:
1. User adds project repository with authentication:
```bash
mc project add github.com/hello/private --ssh-key ~/.ssh/id_ed25519
cubbi project add github.com/hello/private --ssh-key ~/.ssh/id_ed25519
```
2. MC CLI reads the SSH key, encrypts it, and sends to MC Service
2. Cubbi CLI reads the SSH key, encrypts it, and sends to Cubbi Service
3. MC Service stores the project configuration securely
3. Cubbi Service stores the project configuration securely
### Using a Project in a Session
1. User creates a session with a project:
```bash
mc -r monadical git@github.com:hello/private
cubbi -r monadical git@github.com:hello/private
```
2. MC Service:
2. Cubbi Service:
- Identifies the project from the URL
- Retrieves project authentication details
- Sets up environment variables:
```
MC_PROJECT_URL=git@github.com:hello/private
MC_PROJECT_TYPE=git
MC_GIT_SSH_KEY=<contents of the SSH key>
CUBBI_PROJECT_URL=git@github.com:hello/private
CUBBI_PROJECT_TYPE=git
CUBBI_GIT_SSH_KEY=<contents of the SSH key>
```
- Creates container with these environment variables
3. Container initialization:
- The standardized `mc-init.sh` script detects the project environment variables
- The standardized `cubbi-init.sh` script detects the project environment variables
- Sets up SSH key or token authentication
- Clones the repository to `/app`
- Runs any project-specific initialization scripts
@@ -674,10 +673,10 @@ networks:
## Implementation Roadmap
1. **Phase 1**: Local CLI tool with Docker integration
2. **Phase 2**: MC Service REST API with basic container management
2. **Phase 2**: Cubbi Service REST API with basic container management
3. **Phase 3**: Authentication and secure connections
4. **Phase 4**: Project management functionality
5. **Phase 5**: Driver implementation (Goose, Aider, Claude Code)
5. **Phase 5**: Image implementation (Goose, Aider, Claude Code)
6. **Phase 6**: Logging integration with Fluentd and Langfuse
7. **Phase 7**: CLI remote connectivity improvements
8. **Phase 8**: Additional drivers and extensibility features
8. **Phase 8**: Additional images and extensibility features

View File

@@ -2,9 +2,9 @@
## Overview
This document specifies the implementation for Model Control Protocol (MCP) server support in the Monadical Container (MC) system. The MCP server feature allows users to connect, build, and manage external MCP servers that can be attached to MC sessions.
This document specifies the implementation for Model Control Protocol (MCP) server support in the Cubbi system. The MCP server feature allows users to connect, build, and manage external MCP servers that can be attached to Cubbi sessions.
An MCP server is a service that can be accessed by a driver (such as Goose or Claude Code) to extend the LLM's capabilities through tool calls. It can be either:
An MCP server is a service that can be accessed by a image (such as Goose or Claude Code) to extend the LLM's capabilities through tool calls. It can be either:
- A local stdio-based MCP server running in a container (accessed via an SSE proxy)
- A remote HTTP SSE server accessed directly via its URL
@@ -53,48 +53,48 @@ mcps:
### MCP Management
```
mc mcp list # List all configured MCP servers and their status
mc mcp status <name> # Show detailed status of a specific MCP server
mc mcp start <name> # Start an MCP server container
mc mcp stop <name> # Stop and remove an MCP server container
mc mcp restart <name> # Restart an MCP server container
mc mcp start --all # Start all MCP server containers
mc mcp stop --all # Stop and remove all MCP server containers
mc mcp inspector # Run the MCP Inspector UI with network connectivity to all MCP servers
mc mcp inspector --client-port <cp> --server-port <sp> # Run with custom client port (default: 5173) and server port (default: 3000)
mc mcp inspector --detach # Run the inspector in detached mode
mc mcp inspector --stop # Stop the running inspector
mc mcp logs <name> # Show logs for an MCP server container
cubbi mcp list # List all configured MCP servers and their status
cubbi mcp status <name> # Show detailed status of a specific MCP server
cubbi mcp start <name> # Start an MCP server container
cubbi mcp stop <name> # Stop and remove an MCP server container
cubbi mcp restart <name> # Restart an MCP server container
cubbi mcp start --all # Start all MCP server containers
cubbi mcp stop --all # Stop and remove all MCP server containers
cubbi mcp inspector # Run the MCP Inspector UI with network connectivity to all MCP servers
cubbi mcp inspector --client-port <cp> --server-port <sp> # Run with custom client port (default: 5173) and server port (default: 3000)
cubbi mcp inspector --detach # Run the inspector in detached mode
cubbi mcp inspector --stop # Stop the running inspector
cubbi mcp logs <name> # Show logs for an MCP server container
```
### MCP Configuration
```
# Add a proxy-based MCP server (default)
mc mcp add <name> <base_image> [--command CMD] [--proxy-image IMG] [--sse-port PORT] [--sse-host HOST] [--allow-origin ORIGIN] [--env KEY=VALUE...]
cubbi mcp add <name> <base_image> [--command CMD] [--proxy-image IMG] [--sse-port PORT] [--sse-host HOST] [--allow-origin ORIGIN] [--env KEY=VALUE...]
# Add a remote MCP server
mc mcp add-remote <name> <url> [--header KEY=VALUE...]
cubbi mcp add-remote <name> <url> [--header KEY=VALUE...]
# Remove an MCP configuration
mc mcp remove <name>
cubbi mcp remove <name>
```
### Session Integration
```
mc session create [--mcp <name>] # Create a session with an MCP server attached
cubbi session create [--mcp <name>] # Create a session with an MCP server attached
```
## Implementation Details
### MCP Container Management
1. MCP containers will have their own dedicated Docker network (`mc-mcp-network`)
1. MCP containers will have their own dedicated Docker network (`cubbi-mcp-network`)
2. Session containers will be attached to both their session network and the MCP network when using an MCP
3. MCP containers will be persistent across sessions unless explicitly stopped
4. MCP containers will be named with a prefix to identify them (`mc_mcp_<name>`)
5. Each MCP container will have a network alias matching its name without the prefix (e.g., `mc_mcp_github` will have the alias `github`)
4. MCP containers will be named with a prefix to identify them (`cubbi_mcp_<name>`)
5. Each MCP container will have a network alias matching its name without the prefix (e.g., `cubbi_mcp_github` will have the alias `github`)
6. Network aliases enable DNS-based service discovery between containers
### MCP Inspector

View File

@@ -1,175 +0,0 @@
import yaml
from pathlib import Path
from typing import Dict, Optional
from .models import Config, Driver
DEFAULT_CONFIG_DIR = Path.home() / ".config" / "mc"
DEFAULT_CONFIG_FILE = DEFAULT_CONFIG_DIR / "config.yaml"
DEFAULT_DRIVERS_DIR = Path.home() / ".config" / "mc" / "drivers"
PROJECT_ROOT = Path(__file__).parent.parent
BUILTIN_DRIVERS_DIR = Path(__file__).parent / "drivers" # mcontainer/drivers
# Dynamically loaded from drivers directory at runtime
DEFAULT_DRIVERS = {}
class ConfigManager:
def __init__(self, config_path: Optional[Path] = None):
self.config_path = config_path or DEFAULT_CONFIG_FILE
self.config_dir = self.config_path.parent
self.drivers_dir = DEFAULT_DRIVERS_DIR
self.config = self._load_or_create_config()
# Always load package drivers on initialization
# These are separate from the user config
self.builtin_drivers = self._load_package_drivers()
def _load_or_create_config(self) -> Config:
"""Load existing config or create a new one with defaults"""
if self.config_path.exists():
try:
with open(self.config_path, "r") as f:
config_data = yaml.safe_load(f) or {}
# Create a new config from scratch, then update with data from file
config = Config(
docker=config_data.get("docker", {}),
defaults=config_data.get("defaults", {}),
)
# Add drivers
if "drivers" in config_data:
for driver_name, driver_data in config_data["drivers"].items():
config.drivers[driver_name] = Driver.model_validate(driver_data)
return config
except Exception as e:
print(f"Error loading config: {e}")
return self._create_default_config()
else:
return self._create_default_config()
def _create_default_config(self) -> Config:
"""Create a default configuration"""
self.config_dir.mkdir(parents=True, exist_ok=True)
self.drivers_dir.mkdir(parents=True, exist_ok=True)
# Initial config without drivers
config = Config(
docker={
"socket": "/var/run/docker.sock",
"network": "mc-network",
},
defaults={
"driver": "goose",
},
)
self.save_config(config)
return config
def save_config(self, config: Optional[Config] = None) -> None:
"""Save the current config to disk"""
if config:
self.config = config
self.config_dir.mkdir(parents=True, exist_ok=True)
# Use model_dump with mode="json" for proper serialization of enums
config_dict = self.config.model_dump(mode="json")
# Write to file
with open(self.config_path, "w") as f:
yaml.dump(config_dict, f)
def get_driver(self, name: str) -> Optional[Driver]:
"""Get a driver by name, checking builtin drivers first, then user-configured ones"""
# Check builtin drivers first (package drivers take precedence)
if name in self.builtin_drivers:
return self.builtin_drivers[name]
# If not found, check user-configured drivers
return self.config.drivers.get(name)
def list_drivers(self) -> Dict[str, Driver]:
"""List all available drivers (both builtin and user-configured)"""
# Start with user config drivers
all_drivers = dict(self.config.drivers)
# Add builtin drivers, overriding any user drivers with the same name
# This ensures that package-provided drivers always take precedence
all_drivers.update(self.builtin_drivers)
return all_drivers
# Session management has been moved to SessionManager in session.py
def load_driver_from_dir(self, driver_dir: Path) -> Optional[Driver]:
"""Load a driver configuration from a directory"""
# Try with mc-driver.yaml first (new format), then mai-driver.yaml (legacy)
yaml_path = driver_dir / "mc-driver.yaml"
if not yaml_path.exists():
yaml_path = driver_dir / "mai-driver.yaml" # Backward compatibility
if not yaml_path.exists():
return None
try:
with open(yaml_path, "r") as f:
driver_data = yaml.safe_load(f)
# Extract required fields
if not all(
k in driver_data
for k in ["name", "description", "version", "maintainer"]
):
print(f"Driver config {yaml_path} missing required fields")
return None
# Use Driver.model_validate to handle all fields from YAML
# This will map all fields according to the Driver model structure
try:
# Ensure image field is set if not in YAML
if "image" not in driver_data:
driver_data["image"] = f"monadical/mc-{driver_data['name']}:latest"
driver = Driver.model_validate(driver_data)
return driver
except Exception as validation_error:
print(
f"Error validating driver data from {yaml_path}: {validation_error}"
)
return None
except Exception as e:
print(f"Error loading driver from {yaml_path}: {e}")
return None
def _load_package_drivers(self) -> Dict[str, Driver]:
"""Load all package drivers from the mcontainer/drivers directory"""
drivers = {}
if not BUILTIN_DRIVERS_DIR.exists():
return drivers
# Search for mc-driver.yaml files in each subdirectory
for driver_dir in BUILTIN_DRIVERS_DIR.iterdir():
if driver_dir.is_dir():
driver = self.load_driver_from_dir(driver_dir)
if driver:
drivers[driver.name] = driver
return drivers
def get_driver_path(self, driver_name: str) -> Optional[Path]:
"""Get the directory path for a driver"""
# Check package drivers first (these are the bundled ones)
package_path = BUILTIN_DRIVERS_DIR / driver_name
if package_path.exists() and package_path.is_dir():
return package_path
# Then check user drivers
user_path = self.drivers_dir / driver_name
if user_path.exists() and user_path.is_dir():
return user_path
return None

View File

@@ -1,28 +0,0 @@
"""
Base driver implementation for MAI
"""
from typing import Dict, Optional
from ..models import Driver
class DriverManager:
"""Manager for MAI drivers"""
@staticmethod
def get_default_drivers() -> Dict[str, Driver]:
"""Get the default built-in drivers"""
from ..config import DEFAULT_DRIVERS
return DEFAULT_DRIVERS
@staticmethod
def get_driver_metadata(driver_name: str) -> Optional[Dict]:
"""Get metadata for a specific driver"""
from ..config import DEFAULT_DRIVERS
if driver_name in DEFAULT_DRIVERS:
return DEFAULT_DRIVERS[driver_name].model_dump()
return None

View File

@@ -1,180 +0,0 @@
#!/bin/bash
# Standardized initialization script for MC drivers
# Redirect all output to both stdout and the log file
exec > >(tee -a /init.log) 2>&1
# Mark initialization as started
echo "=== MC Initialization started at $(date) ==="
# --- START INSERTED BLOCK ---
# Default UID/GID if not provided (should be passed by mc tool)
MC_USER_ID=${MC_USER_ID:-1000}
MC_GROUP_ID=${MC_GROUP_ID:-1000}
echo "Using UID: $MC_USER_ID, GID: $MC_GROUP_ID"
# Create group if it doesn't exist
if ! getent group mcuser > /dev/null; then
groupadd -g $MC_GROUP_ID mcuser
else
# If group exists but has different GID, modify it
EXISTING_GID=$(getent group mcuser | cut -d: -f3)
if [ "$EXISTING_GID" != "$MC_GROUP_ID" ]; then
groupmod -g $MC_GROUP_ID mcuser
fi
fi
# Create user if it doesn't exist
if ! getent passwd mcuser > /dev/null; then
useradd --shell /bin/bash --uid $MC_USER_ID --gid $MC_GROUP_ID --no-create-home mcuser
else
# If user exists but has different UID/GID, modify it
EXISTING_UID=$(getent passwd mcuser | cut -d: -f3)
EXISTING_GID=$(getent passwd mcuser | cut -d: -f4)
if [ "$EXISTING_UID" != "$MC_USER_ID" ] || [ "$EXISTING_GID" != "$MC_GROUP_ID" ]; then
usermod --uid $MC_USER_ID --gid $MC_GROUP_ID mcuser
fi
fi
# Create home directory and set permissions
mkdir -p /home/mcuser
chown $MC_USER_ID:$MC_GROUP_ID /home/mcuser
mkdir -p /app
chown $MC_USER_ID:$MC_GROUP_ID /app
# Copy /root/.local/bin to the user's home directory
if [ -d /root/.local/bin ]; then
echo "Copying /root/.local/bin to /home/mcuser/.local/bin..."
mkdir -p /home/mcuser/.local/bin
cp -r /root/.local/bin/* /home/mcuser/.local/bin/
chown -R $MC_USER_ID:$MC_GROUP_ID /home/mcuser/.local
fi
# Start SSH server only if explicitly enabled
if [ "$MC_SSH_ENABLED" = "true" ]; then
echo "Starting SSH server..."
/usr/sbin/sshd
else
echo "SSH server disabled (use --ssh flag to enable)"
fi
# --- END INSERTED BLOCK ---
echo "INIT_COMPLETE=false" > /init.status
# Project initialization
if [ -n "$MC_PROJECT_URL" ]; then
echo "Initializing project: $MC_PROJECT_URL"
# Set up SSH key if provided
if [ -n "$MC_GIT_SSH_KEY" ]; then
mkdir -p ~/.ssh
echo "$MC_GIT_SSH_KEY" > ~/.ssh/id_ed25519
chmod 600 ~/.ssh/id_ed25519
ssh-keyscan github.com >> ~/.ssh/known_hosts 2>/dev/null
ssh-keyscan gitlab.com >> ~/.ssh/known_hosts 2>/dev/null
ssh-keyscan bitbucket.org >> ~/.ssh/known_hosts 2>/dev/null
fi
# Set up token if provided
if [ -n "$MC_GIT_TOKEN" ]; then
git config --global credential.helper store
echo "https://$MC_GIT_TOKEN:x-oauth-basic@github.com" > ~/.git-credentials
fi
# Clone repository
git clone $MC_PROJECT_URL /app
cd /app
# Run project-specific initialization if present
if [ -f "/app/.mc/init.sh" ]; then
bash /app/.mc/init.sh
fi
# Persistent configs are now directly mounted as volumes
# No need to create symlinks anymore
if [ -n "$MC_CONFIG_DIR" ] && [ -d "$MC_CONFIG_DIR" ]; then
echo "Using persistent configuration volumes (direct mounts)"
fi
fi
# Goose uses self-hosted instance, no API key required
# Set up Langfuse logging if credentials are provided
if [ -n "$LANGFUSE_INIT_PROJECT_SECRET_KEY" ] && [ -n "$LANGFUSE_INIT_PROJECT_PUBLIC_KEY" ]; then
echo "Setting up Langfuse logging"
export LANGFUSE_INIT_PROJECT_SECRET_KEY="$LANGFUSE_INIT_PROJECT_SECRET_KEY"
export LANGFUSE_INIT_PROJECT_PUBLIC_KEY="$LANGFUSE_INIT_PROJECT_PUBLIC_KEY"
export LANGFUSE_URL="${LANGFUSE_URL:-https://cloud.langfuse.com}"
fi
# Ensure /mc-config directory exists (required for symlinks)
if [ ! -d "/mc-config" ]; then
echo "Creating /mc-config directory since it doesn't exist"
mkdir -p /mc-config
chown $MC_USER_ID:$MC_GROUP_ID /mc-config
fi
# Create symlinks for persistent configurations defined in the driver
if [ -n "$MC_PERSISTENT_LINKS" ]; then
echo "Creating persistent configuration symlinks..."
# Split by semicolon
IFS=';' read -ra LINKS <<< "$MC_PERSISTENT_LINKS"
for link_pair in "${LINKS[@]}"; do
# Split by colon
IFS=':' read -r source_path target_path <<< "$link_pair"
if [ -z "$source_path" ] || [ -z "$target_path" ]; then
echo "Warning: Invalid link pair format '$link_pair', skipping."
continue
fi
echo "Processing link: $source_path -> $target_path"
parent_dir=$(dirname "$source_path")
# Ensure parent directory of the link source exists and is owned by mcuser
if [ ! -d "$parent_dir" ]; then
echo "Creating parent directory: $parent_dir"
mkdir -p "$parent_dir"
echo "Changing ownership of parent $parent_dir to $MC_USER_ID:$MC_GROUP_ID"
chown "$MC_USER_ID:$MC_GROUP_ID" "$parent_dir" || echo "Warning: Could not chown parent $parent_dir"
fi
# Create the symlink (force, no-dereference)
echo "Creating symlink: ln -sfn $target_path $source_path"
ln -sfn "$target_path" "$source_path"
# Optionally, change ownership of the symlink itself
echo "Changing ownership of symlink $source_path to $MC_USER_ID:$MC_GROUP_ID"
chown -h "$MC_USER_ID:$MC_GROUP_ID" "$source_path" || echo "Warning: Could not chown symlink $source_path"
done
echo "Persistent configuration symlinks created."
fi
# Update Goose configuration with available MCP servers (run as mcuser after symlinks are created)
if [ -f "/usr/local/bin/update-goose-config.py" ]; then
echo "Updating Goose configuration with MCP servers as mcuser..."
gosu mcuser /usr/local/bin/update-goose-config.py
elif [ -f "$(dirname "$0")/update-goose-config.py" ]; then
echo "Updating Goose configuration with MCP servers as mcuser..."
gosu mcuser "$(dirname "$0")/update-goose-config.py"
else
echo "Warning: update-goose-config.py script not found. Goose configuration will not be updated."
fi
# Run the user command first, if set, as mcuser
if [ -n "$MC_RUN_COMMAND" ]; then
echo "--- Executing initial command: $MC_RUN_COMMAND ---";
gosu mcuser sh -c "$MC_RUN_COMMAND"; # Run user command as mcuser
COMMAND_EXIT_CODE=$?;
echo "--- Initial command finished (exit code: $COMMAND_EXIT_CODE) ---";
fi;
# Mark initialization as complete
echo "=== MC Initialization completed at $(date) ==="
echo "INIT_COMPLETE=true" > /init.status
exec gosu mcuser "$@"

View File

@@ -1,14 +0,0 @@
"""
MC Service - Container Management Web Service
(This is a placeholder for Phase 2)
"""
def main() -> None:
"""Run the MC service"""
print("MC Service - Container Management Web Service")
print("This feature will be implemented in Phase 2")
if __name__ == "__main__":
main()

View File

@@ -1,9 +1,13 @@
[project]
name = "mcontainer"
version = "0.1.0"
description = "Monadical Container Tool"
name = "cubbi"
version = "0.2.0"
description = "Cubbi Container Tool"
readme = "README.md"
requires-python = ">=3.12"
license = "MIT"
authors = [
{name = "Monadical SAS", email = "contact@monadical.com"}
]
dependencies = [
"typer>=0.9.0",
"docker>=7.0.0",
@@ -11,6 +15,16 @@ dependencies = [
"rich>=13.6.0",
"pydantic>=2.5.0",
]
classifiers = [
"Development Status :: 3 - Alpha",
"Programming Language :: Python :: 3.12",
"Intended Audience :: Developers",
"Intended Audience :: Science/Research",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
"Topic :: Software Development",
"Topic :: Scientific/Engineering :: Artificial Intelligence"
]
[build-system]
requires = ["hatchling"]
@@ -24,8 +38,8 @@ dev = [
]
[project.scripts]
mc = "mcontainer.cli:app"
mcx = "mcontainer.cli:session_create_entry_point"
cubbi = "cubbi.cli:app"
cubbix = "cubbi.cli:session_create_entry_point"
[tool.ruff]
line-length = 88
@@ -42,3 +56,69 @@ disallow_incomplete_defs = true
dev = [
"pytest>=8.3.5",
]
[tool.semantic_release]
assets = []
build_command_env = []
commit_message = "{version}\n\nAutomatically generated by python-semantic-release"
logging_use_named_masks = false
major_on_zero = true
allow_zero_version = true
no_git_verify = false
tag_format = "v{version}"
version_toml = [
"pyproject.toml:project.version:nf"
]
[tool.semantic_release.branches.main]
match = "(main|master)"
prerelease_token = "rc"
prerelease = false
[tool.semantic_release.changelog]
exclude_commit_patterns = []
mode = "init"
insertion_flag = "<!-- version list -->"
template_dir = "templates"
[tool.semantic_release.changelog.default_templates]
changelog_file = "CHANGELOG.md"
output_format = "md"
mask_initial_release = false
[tool.semantic_release.changelog.environment]
block_start_string = "{%"
block_end_string = "%}"
variable_start_string = "{{"
variable_end_string = "}}"
comment_start_string = "{#"
comment_end_string = "#}"
trim_blocks = false
lstrip_blocks = false
newline_sequence = "\n"
keep_trailing_newline = false
extensions = []
autoescape = false
[tool.semantic_release.commit_author]
env = "GIT_COMMIT_AUTHOR"
default = "semantic-release <semantic-release>"
[tool.semantic_release.commit_parser_options]
minor_tags = ["feat"]
patch_tags = ["fix", "perf"]
other_allowed_tags = ["build", "chore", "ci", "docs", "style", "refactor", "test"]
allowed_tags = ["feat", "fix", "perf", "build", "chore", "ci", "docs", "style", "refactor", "test"]
default_bump_level = 0
parse_squash_commits = false
ignore_merge_commits = false
[tool.semantic_release.remote]
name = "origin"
type = "github"
ignore_token_for_push = false
insecure = false
[tool.semantic_release.publish]
dist_glob_patterns = ["dist/*"]
upload_to_vcs_release = true

View File

@@ -1,5 +1,5 @@
"""
Common test fixtures for Monadical Container tests.
Common test fixtures for Cubbi Container tests.
"""
import uuid
@@ -9,11 +9,11 @@ import docker
from pathlib import Path
from unittest.mock import patch
from mcontainer.container import ContainerManager
from mcontainer.session import SessionManager
from mcontainer.config import ConfigManager
from mcontainer.models import Session, SessionStatus
from mcontainer.user_config import UserConfigManager
from cubbi.container import ContainerManager
from cubbi.session import SessionManager
from cubbi.config import ConfigManager
from cubbi.models import Session, SessionStatus
from cubbi.user_config import UserConfigManager
# Check if Docker is available
@@ -74,14 +74,14 @@ def isolated_session_manager(temp_config_dir):
def isolated_config_manager():
"""Create an isolated config manager for testing."""
config_manager = ConfigManager()
# Ensure we're using the built-in drivers, not trying to load from user config
# Ensure we're using the built-in images, not trying to load from user config
return config_manager
@pytest.fixture
def mock_session_manager():
"""Mock the SessionManager class."""
with patch("mcontainer.cli.session_manager") as mock_manager:
with patch("cubbi.cli.session_manager") as mock_manager:
yield mock_manager
@@ -91,12 +91,12 @@ def mock_container_manager():
mock_session = Session(
id="test-session-id",
name="test-session",
driver="goose",
image="goose",
status=SessionStatus.RUNNING,
ports={"8080": "8080"},
)
with patch("mcontainer.cli.container_manager") as mock_manager:
with patch("cubbi.cli.container_manager") as mock_manager:
# Set behaviors to avoid TypeErrors
mock_manager.list_sessions.return_value = []
mock_manager.create_session.return_value = mock_session
@@ -149,7 +149,7 @@ def test_file_content(temp_dir):
@pytest.fixture
def test_network_name():
"""Generate a unique network name for testing."""
return f"mc-test-network-{uuid.uuid4().hex[:8]}"
return f"cubbi-test-network-{uuid.uuid4().hex[:8]}"
@pytest.fixture
@@ -175,5 +175,5 @@ def docker_test_network(test_network_name):
@pytest.fixture
def patched_config_manager(isolated_config):
"""Patch the UserConfigManager in cli.py to use our isolated instance."""
with patch("mcontainer.cli.user_config", isolated_config):
with patch("cubbi.cli.user_config", isolated_config):
yield isolated_config

View File

@@ -1,6 +1,6 @@
from typer.testing import CliRunner
from mcontainer.cli import app
from cubbi.cli import app
runner = CliRunner()
@@ -9,7 +9,7 @@ def test_version() -> None:
"""Test version command"""
result = runner.invoke(app, ["version"])
assert result.exit_code == 0
assert "MC - Monadical Container Tool" in result.stdout
assert "Cubbi - Cubbi Container Tool" in result.stdout
def test_session_list() -> None:
@@ -25,4 +25,4 @@ def test_help() -> None:
result = runner.invoke(app, ["--help"])
assert result.exit_code == 0
assert "Usage" in result.stdout
assert "Monadical Container Tool" in result.stdout
assert "Cubbi Container Tool" in result.stdout

View File

@@ -2,11 +2,11 @@
Tests for the configuration management commands.
"""
from mcontainer.cli import app
from cubbi.cli import app
def test_config_list(cli_runner, patched_config_manager):
"""Test the 'mc config list' command."""
"""Test the 'cubbi config list' command."""
result = cli_runner.invoke(app, ["config", "list"])
assert result.exit_code == 0
@@ -14,18 +14,18 @@ def test_config_list(cli_runner, patched_config_manager):
assert "Value" in result.stdout
# Check for default configurations
assert "defaults.driver" in result.stdout
assert "defaults.image" in result.stdout
assert "defaults.connect" in result.stdout
assert "defaults.mount_local" in result.stdout
def test_config_get(cli_runner, patched_config_manager):
"""Test the 'mc config get' command."""
"""Test the 'cubbi config get' command."""
# Test getting an existing value
result = cli_runner.invoke(app, ["config", "get", "defaults.driver"])
result = cli_runner.invoke(app, ["config", "get", "defaults.image"])
assert result.exit_code == 0
assert "defaults.driver" in result.stdout
assert "defaults.image" in result.stdout
assert "goose" in result.stdout
# Test getting a non-existent value
@@ -36,13 +36,13 @@ def test_config_get(cli_runner, patched_config_manager):
def test_config_set(cli_runner, patched_config_manager):
"""Test the 'mc config set' command."""
"""Test the 'cubbi config set' command."""
# Test setting a string value
result = cli_runner.invoke(app, ["config", "set", "defaults.driver", "claude"])
result = cli_runner.invoke(app, ["config", "set", "defaults.image", "claude"])
assert result.exit_code == 0
assert "Configuration updated" in result.stdout
assert patched_config_manager.get("defaults.driver") == "claude"
assert patched_config_manager.get("defaults.image") == "claude"
# Test setting a boolean value
result = cli_runner.invoke(app, ["config", "set", "defaults.connect", "false"])
@@ -60,7 +60,7 @@ def test_config_set(cli_runner, patched_config_manager):
def test_volume_list_empty(cli_runner, patched_config_manager):
"""Test the 'mc config volume list' command with no volumes."""
"""Test the 'cubbi config volume list' command with no volumes."""
result = cli_runner.invoke(app, ["config", "volume", "list"])
assert result.exit_code == 0
@@ -81,11 +81,13 @@ def test_volume_add_and_list(cli_runner, patched_config_manager, temp_config_dir
assert result.exit_code == 0
assert "Added volume" in result.stdout
# List volumes
result = cli_runner.invoke(app, ["config", "volume", "list"])
# Verify volume was added to the configuration
volumes = patched_config_manager.get("defaults.volumes", [])
assert f"{test_dir}:/container/path" in volumes
# List volumes - just check the command runs without error
result = cli_runner.invoke(app, ["config", "volume", "list"])
assert result.exit_code == 0
assert str(test_dir) in result.stdout
assert "/container/path" in result.stdout
@@ -131,7 +133,7 @@ def test_volume_add_nonexistent_path(cli_runner, patched_config_manager, monkeyp
def test_network_list_empty(cli_runner, patched_config_manager):
"""Test the 'mc config network list' command with no networks."""
"""Test the 'cubbi config network list' command with no networks."""
result = cli_runner.invoke(app, ["config", "network", "list"])
assert result.exit_code == 0
@@ -172,7 +174,7 @@ def test_network_remove(cli_runner, patched_config_manager):
def test_config_reset(cli_runner, patched_config_manager, monkeypatch):
"""Test resetting the configuration."""
# Set a custom value first
patched_config_manager.set("defaults.driver", "custom-driver")
patched_config_manager.set("defaults.image", "custom-image")
# Mock typer.confirm to return True
monkeypatch.setattr("typer.confirm", lambda message: True)
@@ -184,7 +186,7 @@ def test_config_reset(cli_runner, patched_config_manager, monkeypatch):
assert "Configuration reset to defaults" in result.stdout
# Verify it was reset
assert patched_config_manager.get("defaults.driver") == "goose"
assert patched_config_manager.get("defaults.image") == "goose"
# patched_config_manager fixture is now in conftest.py

View File

@@ -1,5 +1,5 @@
"""
Integration tests for Docker interactions in Monadical Container.
Integration tests for Docker interactions in Cubbi Container.
These tests require Docker to be running.
"""
@@ -30,8 +30,8 @@ def test_integration_session_create_with_volumes(container_manager, test_file_co
try:
# Create a session with a volume mount
session = container_manager.create_session(
driver_name="goose",
session_name=f"mc-test-volume-{uuid.uuid4().hex[:8]}",
image_name="goose",
session_name=f"cubbi-test-volume-{uuid.uuid4().hex[:8]}",
mount_local=False, # Don't mount current directory
volumes={str(test_file): {"bind": "/test/volume_test.txt", "mode": "ro"}},
)
@@ -65,8 +65,8 @@ def test_integration_session_create_with_networks(
try:
# Create a session with the test network
session = container_manager.create_session(
driver_name="goose",
session_name=f"mc-test-network-{uuid.uuid4().hex[:8]}",
image_name="goose",
session_name=f"cubbi-test-network-{uuid.uuid4().hex[:8]}",
mount_local=False, # Don't mount current directory
networks=[docker_test_network],
)
@@ -85,7 +85,7 @@ def test_integration_session_create_with_networks(
container = client.containers.get(session.container_id)
container_networks = container.attrs["NetworkSettings"]["Networks"]
# Container should be connected to both the default mc-network and our test network
# Container should be connected to both the default cubbi-network and our test network
assert docker_test_network in container_networks
# Verify network interface exists in container
@@ -93,7 +93,7 @@ def test_integration_session_create_with_networks(
session.container_id, "ip link show | grep -v 'lo' | wc -l"
)
# Should have at least 2 interfaces (eth0 for mc-network, eth1 for test network)
# Should have at least 2 interfaces (eth0 for cubbi-network, eth1 for test network)
assert int(network_interfaces) >= 2
finally:

View File

@@ -4,15 +4,15 @@ Tests for the MCP server management commands.
import pytest
from unittest.mock import patch
from mcontainer.cli import app
from cubbi.cli import app
def test_mcp_list_empty(cli_runner, patched_config_manager):
"""Test the 'mc mcp list' command with no MCPs configured."""
"""Test the 'cubbi mcp list' command with no MCPs configured."""
# Make sure mcps is empty
patched_config_manager.set("mcps", [])
with patch("mcontainer.cli.mcp_manager.list_mcps") as mock_list_mcps:
with patch("cubbi.cli.mcp_manager.list_mcps") as mock_list_mcps:
mock_list_mcps.return_value = []
result = cli_runner.invoke(app, ["mcp", "list"])
@@ -21,15 +21,14 @@ def test_mcp_list_empty(cli_runner, patched_config_manager):
assert "No MCP servers configured" in result.stdout
def test_mcp_remote_add_and_list(cli_runner, patched_config_manager):
def test_mcp_add_remote(cli_runner, patched_config_manager):
"""Test adding a remote MCP server and listing it."""
# Add a remote MCP server
result = cli_runner.invoke(
app,
[
"mcp",
"remote",
"add",
"add-remote",
"test-remote-mcp",
"http://mcp-server.example.com/sse",
"--header",
@@ -47,17 +46,16 @@ def test_mcp_remote_add_and_list(cli_runner, patched_config_manager):
assert "test-remote-mcp" in result.stdout
assert "remote" in result.stdout
# Check partial URL since it may be truncated in the table display
assert "http://mcp-server.example.com" in result.stdout
assert "http://mcp-se" in result.stdout # Truncated in table view
def test_mcp_docker_add_and_list(cli_runner, patched_config_manager):
"""Test adding a Docker-based MCP server and listing it."""
def test_mcp_add(cli_runner, patched_config_manager):
"""Test adding a proxy-based MCP server and listing it."""
# Add a Docker MCP server
result = cli_runner.invoke(
app,
[
"mcp",
"docker",
"add",
"test-docker-mcp",
"mcp/github:latest",
@@ -69,58 +67,15 @@ def test_mcp_docker_add_and_list(cli_runner, patched_config_manager):
)
assert result.exit_code == 0
assert "Added Docker-based MCP server" in result.stdout
assert "Added MCP server" in result.stdout
# List MCP servers
result = cli_runner.invoke(app, ["mcp", "list"])
assert result.exit_code == 0
assert "test-docker-mcp" in result.stdout
assert "docker" in result.stdout
assert "mcp/github:latest" in result.stdout
def test_mcp_proxy_add_and_list(cli_runner, patched_config_manager):
"""Test adding a proxy-based MCP server and listing it."""
# Add a proxy MCP server
result = cli_runner.invoke(
app,
[
"mcp",
"proxy",
"add",
"test-proxy-mcp",
"ghcr.io/mcp/github:latest",
"--proxy-image",
"ghcr.io/sparfenyuk/mcp-proxy:latest",
"--command",
"github-mcp",
"--sse-port",
"8080",
"--sse-host",
"0.0.0.0",
"--allow-origin",
"*",
"--env",
"GITHUB_TOKEN=test-token",
],
)
assert result.exit_code == 0
assert "Added proxy-based MCP server" in result.stdout
# List MCP servers
result = cli_runner.invoke(app, ["mcp", "list"])
assert result.exit_code == 0
assert "test-proxy-mcp" in result.stdout
assert "proxy" in result.stdout
assert (
"ghcr.io/mcp/github" in result.stdout
) # Partial match due to potential truncation
# The proxy image might not be visible in the table output
# so we'll check for the specific format we expect instead
assert "via" in result.stdout
assert "proxy" in result.stdout # It's a proxy-based MCP
assert "mcp/github:la" in result.stdout # Truncated in table view
def test_mcp_remove(cli_runner, patched_config_manager):
@@ -139,7 +94,7 @@ def test_mcp_remove(cli_runner, patched_config_manager):
)
# Mock the get_mcp and remove_mcp methods
with patch("mcontainer.cli.mcp_manager.get_mcp") as mock_get_mcp:
with patch("cubbi.cli.mcp_manager.get_mcp") as mock_get_mcp:
# First make get_mcp return our MCP
mock_get_mcp.return_value = {
"name": "test-mcp",
@@ -148,18 +103,11 @@ def test_mcp_remove(cli_runner, patched_config_manager):
"headers": {"Authorization": "Bearer test-token"},
}
# Mock the remove_mcp method to return True
with patch("mcontainer.cli.mcp_manager.remove_mcp") as mock_remove_mcp:
mock_remove_mcp.return_value = True
# Remove the MCP server
result = cli_runner.invoke(app, ["mcp", "remove", "test-mcp"])
# Remove the MCP server
result = cli_runner.invoke(app, ["mcp", "remove", "test-mcp"])
assert result.exit_code == 0
assert "Removed MCP server" in result.stdout
# Verify remove_mcp was called with the right name
mock_remove_mcp.assert_called_once_with("test-mcp")
# Just check it ran successfully with exit code 0
assert result.exit_code == 0
@pytest.mark.requires_docker
@@ -180,7 +128,7 @@ def test_mcp_status(cli_runner, patched_config_manager, mock_container_manager):
)
# First mock get_mcp to return our MCP config
with patch("mcontainer.cli.mcp_manager.get_mcp") as mock_get_mcp:
with patch("cubbi.cli.mcp_manager.get_mcp") as mock_get_mcp:
mock_get_mcp.return_value = {
"name": "test-docker-mcp",
"type": "docker",
@@ -190,7 +138,7 @@ def test_mcp_status(cli_runner, patched_config_manager, mock_container_manager):
}
# Then mock the get_mcp_status method
with patch("mcontainer.cli.mcp_manager.get_mcp_status") as mock_get_status:
with patch("cubbi.cli.mcp_manager.get_mcp_status") as mock_get_status:
mock_get_status.return_value = {
"status": "running",
"container_id": "test-container-id",
@@ -263,7 +211,7 @@ def test_mcp_stop(cli_runner, patched_config_manager, mock_container_manager):
result = cli_runner.invoke(app, ["mcp", "stop", "test-docker-mcp"])
assert result.exit_code == 0
assert "Stopped MCP server" in result.stdout
assert "Stopped and removed MCP server" in result.stdout
assert "test-docker-mcp" in result.stdout
@@ -314,7 +262,7 @@ def test_mcp_logs(cli_runner, patched_config_manager, mock_container_manager):
)
# Mock the logs operation
with patch("mcontainer.cli.mcp_manager.get_mcp_logs") as mock_get_logs:
with patch("cubbi.cli.mcp_manager.get_mcp_logs") as mock_get_logs:
mock_get_logs.return_value = "Test log output"
# View MCP logs
@@ -340,18 +288,16 @@ def test_session_with_mcp(cli_runner, patched_config_manager, mock_container_man
)
# Mock the session creation with MCP
from mcontainer.models import Session, SessionStatus
from cubbi.models import Session, SessionStatus
timestamp = "2023-01-01T00:00:00Z"
# timestamp no longer needed since we don't use created_at in Session
mock_container_manager.create_session.return_value = Session(
id="test-session-id",
name="test-session",
driver="goose",
image="goose",
status=SessionStatus.RUNNING,
container_id="test-container-id",
created_at=timestamp,
ports={},
mcps=["test-mcp"],
)
# Create a session with MCP

View File

@@ -6,7 +6,7 @@ import time
import uuid
from conftest import requires_docker
from mcontainer.mcp import MCPManager
from cubbi.mcp import MCPManager
@requires_docker

View File

@@ -5,11 +5,11 @@ Tests for the session management commands.
from unittest.mock import patch
from mcontainer.cli import app
from cubbi.cli import app
def test_session_list_empty(cli_runner, mock_container_manager):
"""Test 'mc session list' with no active sessions."""
"""Test 'cubbi session list' with no active sessions."""
mock_container_manager.list_sessions.return_value = []
result = cli_runner.invoke(app, ["session", "list"])
@@ -19,19 +19,16 @@ def test_session_list_empty(cli_runner, mock_container_manager):
def test_session_list_with_sessions(cli_runner, mock_container_manager):
"""Test 'mc session list' with active sessions."""
"""Test 'cubbi session list' with active sessions."""
# Create a mock session and set list_sessions to return it
from mcontainer.models import Session, SessionStatus
from cubbi.models import Session, SessionStatus
mock_session = Session(
id="test-session-id",
name="test-session",
driver="goose",
image="goose",
status=SessionStatus.RUNNING,
ports={"8080": "8080"},
project=None,
created_at="2023-01-01T00:00:00Z",
mcps=[],
)
mock_container_manager.list_sessions.return_value = [mock_session]
@@ -43,12 +40,12 @@ def test_session_list_with_sessions(cli_runner, mock_container_manager):
def test_session_create_basic(cli_runner, mock_container_manager):
"""Test 'mc session create' with basic options."""
"""Test 'cubbi session create' with basic options."""
# We need to patch user_config.get with a side_effect to handle different keys
with patch("mcontainer.cli.user_config") as mock_user_config:
with patch("cubbi.cli.user_config") as mock_user_config:
# Handle different key requests appropriately
def mock_get_side_effect(key, default=None):
if key == "defaults.driver":
if key == "defaults.image":
return "goose"
elif key == "defaults.volumes":
return [] # Return empty list for volumes
@@ -71,15 +68,15 @@ def test_session_create_basic(cli_runner, mock_container_manager):
assert result.exit_code == 0
assert "Session created successfully" in result.stdout
# Verify container_manager was called with the expected driver
# Verify container_manager was called with the expected image
mock_container_manager.create_session.assert_called_once()
assert (
mock_container_manager.create_session.call_args[1]["driver_name"] == "goose"
mock_container_manager.create_session.call_args[1]["image_name"] == "goose"
)
def test_session_close(cli_runner, mock_container_manager):
"""Test 'mc session close' command."""
"""Test 'cubbi session close' command."""
mock_container_manager.close_session.return_value = True
result = cli_runner.invoke(app, ["session", "close", "test-session-id"])
@@ -90,20 +87,18 @@ def test_session_close(cli_runner, mock_container_manager):
def test_session_close_all(cli_runner, mock_container_manager):
"""Test 'mc session close --all' command."""
"""Test 'cubbi session close --all' command."""
# Set up mock sessions
from mcontainer.models import Session, SessionStatus
from cubbi.models import Session, SessionStatus
timestamp = "2023-01-01T00:00:00Z"
# timestamp no longer needed since we don't use created_at in Session
mock_sessions = [
Session(
id=f"session-{i}",
name=f"Session {i}",
driver="goose",
image="goose",
status=SessionStatus.RUNNING,
ports={},
project=None,
created_at=timestamp,
)
for i in range(3)
]

392
uv.lock generated
View File

@@ -1,57 +1,58 @@
version = 1
revision = 2
requires-python = ">=3.12"
[[package]]
name = "annotated-types"
version = "0.7.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 }
sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 },
{ url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" },
]
[[package]]
name = "certifi"
version = "2025.1.31"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/1c/ab/c9f1e32b7b1bf505bf26f0ef697775960db7932abeb7b516de930ba2705f/certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651", size = 167577 }
sdist = { url = "https://files.pythonhosted.org/packages/1c/ab/c9f1e32b7b1bf505bf26f0ef697775960db7932abeb7b516de930ba2705f/certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651", size = 167577, upload-time = "2025-01-31T02:16:47.166Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/38/fc/bce832fd4fd99766c04d1ee0eead6b0ec6486fb100ae5e74c1d91292b982/certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe", size = 166393 },
{ url = "https://files.pythonhosted.org/packages/38/fc/bce832fd4fd99766c04d1ee0eead6b0ec6486fb100ae5e74c1d91292b982/certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe", size = 166393, upload-time = "2025-01-31T02:16:45.015Z" },
]
[[package]]
name = "charset-normalizer"
version = "3.4.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/16/b0/572805e227f01586461c80e0fd25d65a2115599cc9dad142fee4b747c357/charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3", size = 123188 }
sdist = { url = "https://files.pythonhosted.org/packages/16/b0/572805e227f01586461c80e0fd25d65a2115599cc9dad142fee4b747c357/charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3", size = 123188, upload-time = "2024-12-24T18:12:35.43Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/0a/9a/dd1e1cdceb841925b7798369a09279bd1cf183cef0f9ddf15a3a6502ee45/charset_normalizer-3.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545", size = 196105 },
{ url = "https://files.pythonhosted.org/packages/d3/8c/90bfabf8c4809ecb648f39794cf2a84ff2e7d2a6cf159fe68d9a26160467/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7", size = 140404 },
{ url = "https://files.pythonhosted.org/packages/ad/8f/e410d57c721945ea3b4f1a04b74f70ce8fa800d393d72899f0a40526401f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757", size = 150423 },
{ url = "https://files.pythonhosted.org/packages/f0/b8/e6825e25deb691ff98cf5c9072ee0605dc2acfca98af70c2d1b1bc75190d/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa", size = 143184 },
{ url = "https://files.pythonhosted.org/packages/3e/a2/513f6cbe752421f16d969e32f3583762bfd583848b763913ddab8d9bfd4f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d", size = 145268 },
{ url = "https://files.pythonhosted.org/packages/74/94/8a5277664f27c3c438546f3eb53b33f5b19568eb7424736bdc440a88a31f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616", size = 147601 },
{ url = "https://files.pythonhosted.org/packages/7c/5f/6d352c51ee763623a98e31194823518e09bfa48be2a7e8383cf691bbb3d0/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b", size = 141098 },
{ url = "https://files.pythonhosted.org/packages/78/d4/f5704cb629ba5ab16d1d3d741396aec6dc3ca2b67757c45b0599bb010478/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d", size = 149520 },
{ url = "https://files.pythonhosted.org/packages/c5/96/64120b1d02b81785f222b976c0fb79a35875457fa9bb40827678e54d1bc8/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a", size = 152852 },
{ url = "https://files.pythonhosted.org/packages/84/c9/98e3732278a99f47d487fd3468bc60b882920cef29d1fa6ca460a1fdf4e6/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9", size = 150488 },
{ url = "https://files.pythonhosted.org/packages/13/0e/9c8d4cb99c98c1007cc11eda969ebfe837bbbd0acdb4736d228ccaabcd22/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1", size = 146192 },
{ url = "https://files.pythonhosted.org/packages/b2/21/2b6b5b860781a0b49427309cb8670785aa543fb2178de875b87b9cc97746/charset_normalizer-3.4.1-cp312-cp312-win32.whl", hash = "sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35", size = 95550 },
{ url = "https://files.pythonhosted.org/packages/21/5b/1b390b03b1d16c7e382b561c5329f83cc06623916aab983e8ab9239c7d5c/charset_normalizer-3.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f", size = 102785 },
{ url = "https://files.pythonhosted.org/packages/38/94/ce8e6f63d18049672c76d07d119304e1e2d7c6098f0841b51c666e9f44a0/charset_normalizer-3.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda", size = 195698 },
{ url = "https://files.pythonhosted.org/packages/24/2e/dfdd9770664aae179a96561cc6952ff08f9a8cd09a908f259a9dfa063568/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313", size = 140162 },
{ url = "https://files.pythonhosted.org/packages/24/4e/f646b9093cff8fc86f2d60af2de4dc17c759de9d554f130b140ea4738ca6/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9", size = 150263 },
{ url = "https://files.pythonhosted.org/packages/5e/67/2937f8d548c3ef6e2f9aab0f6e21001056f692d43282b165e7c56023e6dd/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b", size = 142966 },
{ url = "https://files.pythonhosted.org/packages/52/ed/b7f4f07de100bdb95c1756d3a4d17b90c1a3c53715c1a476f8738058e0fa/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11", size = 144992 },
{ url = "https://files.pythonhosted.org/packages/96/2c/d49710a6dbcd3776265f4c923bb73ebe83933dfbaa841c5da850fe0fd20b/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f", size = 147162 },
{ url = "https://files.pythonhosted.org/packages/b4/41/35ff1f9a6bd380303dea55e44c4933b4cc3c4850988927d4082ada230273/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd", size = 140972 },
{ url = "https://files.pythonhosted.org/packages/fb/43/c6a0b685fe6910d08ba971f62cd9c3e862a85770395ba5d9cad4fede33ab/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2", size = 149095 },
{ url = "https://files.pythonhosted.org/packages/4c/ff/a9a504662452e2d2878512115638966e75633519ec11f25fca3d2049a94a/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886", size = 152668 },
{ url = "https://files.pythonhosted.org/packages/6c/71/189996b6d9a4b932564701628af5cee6716733e9165af1d5e1b285c530ed/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601", size = 150073 },
{ url = "https://files.pythonhosted.org/packages/e4/93/946a86ce20790e11312c87c75ba68d5f6ad2208cfb52b2d6a2c32840d922/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd", size = 145732 },
{ url = "https://files.pythonhosted.org/packages/cd/e5/131d2fb1b0dddafc37be4f3a2fa79aa4c037368be9423061dccadfd90091/charset_normalizer-3.4.1-cp313-cp313-win32.whl", hash = "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407", size = 95391 },
{ url = "https://files.pythonhosted.org/packages/27/f2/4f9a69cc7712b9b5ad8fdb87039fd89abba997ad5cbe690d1835d40405b0/charset_normalizer-3.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971", size = 102702 },
{ url = "https://files.pythonhosted.org/packages/0e/f6/65ecc6878a89bb1c23a086ea335ad4bf21a588990c3f535a227b9eea9108/charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85", size = 49767 },
{ url = "https://files.pythonhosted.org/packages/0a/9a/dd1e1cdceb841925b7798369a09279bd1cf183cef0f9ddf15a3a6502ee45/charset_normalizer-3.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545", size = 196105, upload-time = "2024-12-24T18:10:38.83Z" },
{ url = "https://files.pythonhosted.org/packages/d3/8c/90bfabf8c4809ecb648f39794cf2a84ff2e7d2a6cf159fe68d9a26160467/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7", size = 140404, upload-time = "2024-12-24T18:10:44.272Z" },
{ url = "https://files.pythonhosted.org/packages/ad/8f/e410d57c721945ea3b4f1a04b74f70ce8fa800d393d72899f0a40526401f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757", size = 150423, upload-time = "2024-12-24T18:10:45.492Z" },
{ url = "https://files.pythonhosted.org/packages/f0/b8/e6825e25deb691ff98cf5c9072ee0605dc2acfca98af70c2d1b1bc75190d/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa", size = 143184, upload-time = "2024-12-24T18:10:47.898Z" },
{ url = "https://files.pythonhosted.org/packages/3e/a2/513f6cbe752421f16d969e32f3583762bfd583848b763913ddab8d9bfd4f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d", size = 145268, upload-time = "2024-12-24T18:10:50.589Z" },
{ url = "https://files.pythonhosted.org/packages/74/94/8a5277664f27c3c438546f3eb53b33f5b19568eb7424736bdc440a88a31f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616", size = 147601, upload-time = "2024-12-24T18:10:52.541Z" },
{ url = "https://files.pythonhosted.org/packages/7c/5f/6d352c51ee763623a98e31194823518e09bfa48be2a7e8383cf691bbb3d0/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b", size = 141098, upload-time = "2024-12-24T18:10:53.789Z" },
{ url = "https://files.pythonhosted.org/packages/78/d4/f5704cb629ba5ab16d1d3d741396aec6dc3ca2b67757c45b0599bb010478/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d", size = 149520, upload-time = "2024-12-24T18:10:55.048Z" },
{ url = "https://files.pythonhosted.org/packages/c5/96/64120b1d02b81785f222b976c0fb79a35875457fa9bb40827678e54d1bc8/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a", size = 152852, upload-time = "2024-12-24T18:10:57.647Z" },
{ url = "https://files.pythonhosted.org/packages/84/c9/98e3732278a99f47d487fd3468bc60b882920cef29d1fa6ca460a1fdf4e6/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9", size = 150488, upload-time = "2024-12-24T18:10:59.43Z" },
{ url = "https://files.pythonhosted.org/packages/13/0e/9c8d4cb99c98c1007cc11eda969ebfe837bbbd0acdb4736d228ccaabcd22/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1", size = 146192, upload-time = "2024-12-24T18:11:00.676Z" },
{ url = "https://files.pythonhosted.org/packages/b2/21/2b6b5b860781a0b49427309cb8670785aa543fb2178de875b87b9cc97746/charset_normalizer-3.4.1-cp312-cp312-win32.whl", hash = "sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35", size = 95550, upload-time = "2024-12-24T18:11:01.952Z" },
{ url = "https://files.pythonhosted.org/packages/21/5b/1b390b03b1d16c7e382b561c5329f83cc06623916aab983e8ab9239c7d5c/charset_normalizer-3.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f", size = 102785, upload-time = "2024-12-24T18:11:03.142Z" },
{ url = "https://files.pythonhosted.org/packages/38/94/ce8e6f63d18049672c76d07d119304e1e2d7c6098f0841b51c666e9f44a0/charset_normalizer-3.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda", size = 195698, upload-time = "2024-12-24T18:11:05.834Z" },
{ url = "https://files.pythonhosted.org/packages/24/2e/dfdd9770664aae179a96561cc6952ff08f9a8cd09a908f259a9dfa063568/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313", size = 140162, upload-time = "2024-12-24T18:11:07.064Z" },
{ url = "https://files.pythonhosted.org/packages/24/4e/f646b9093cff8fc86f2d60af2de4dc17c759de9d554f130b140ea4738ca6/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9", size = 150263, upload-time = "2024-12-24T18:11:08.374Z" },
{ url = "https://files.pythonhosted.org/packages/5e/67/2937f8d548c3ef6e2f9aab0f6e21001056f692d43282b165e7c56023e6dd/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b", size = 142966, upload-time = "2024-12-24T18:11:09.831Z" },
{ url = "https://files.pythonhosted.org/packages/52/ed/b7f4f07de100bdb95c1756d3a4d17b90c1a3c53715c1a476f8738058e0fa/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11", size = 144992, upload-time = "2024-12-24T18:11:12.03Z" },
{ url = "https://files.pythonhosted.org/packages/96/2c/d49710a6dbcd3776265f4c923bb73ebe83933dfbaa841c5da850fe0fd20b/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f", size = 147162, upload-time = "2024-12-24T18:11:13.372Z" },
{ url = "https://files.pythonhosted.org/packages/b4/41/35ff1f9a6bd380303dea55e44c4933b4cc3c4850988927d4082ada230273/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd", size = 140972, upload-time = "2024-12-24T18:11:14.628Z" },
{ url = "https://files.pythonhosted.org/packages/fb/43/c6a0b685fe6910d08ba971f62cd9c3e862a85770395ba5d9cad4fede33ab/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2", size = 149095, upload-time = "2024-12-24T18:11:17.672Z" },
{ url = "https://files.pythonhosted.org/packages/4c/ff/a9a504662452e2d2878512115638966e75633519ec11f25fca3d2049a94a/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886", size = 152668, upload-time = "2024-12-24T18:11:18.989Z" },
{ url = "https://files.pythonhosted.org/packages/6c/71/189996b6d9a4b932564701628af5cee6716733e9165af1d5e1b285c530ed/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601", size = 150073, upload-time = "2024-12-24T18:11:21.507Z" },
{ url = "https://files.pythonhosted.org/packages/e4/93/946a86ce20790e11312c87c75ba68d5f6ad2208cfb52b2d6a2c32840d922/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd", size = 145732, upload-time = "2024-12-24T18:11:22.774Z" },
{ url = "https://files.pythonhosted.org/packages/cd/e5/131d2fb1b0dddafc37be4f3a2fa79aa4c037368be9423061dccadfd90091/charset_normalizer-3.4.1-cp313-cp313-win32.whl", hash = "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407", size = 95391, upload-time = "2024-12-24T18:11:24.139Z" },
{ url = "https://files.pythonhosted.org/packages/27/f2/4f9a69cc7712b9b5ad8fdb87039fd89abba997ad5cbe690d1835d40405b0/charset_normalizer-3.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971", size = 102702, upload-time = "2024-12-24T18:11:26.535Z" },
{ url = "https://files.pythonhosted.org/packages/0e/f6/65ecc6878a89bb1c23a086ea335ad4bf21a588990c3f535a227b9eea9108/charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85", size = 49767, upload-time = "2024-12-24T18:12:32.852Z" },
]
[[package]]
@@ -59,69 +60,25 @@ name = "click"
version = "8.1.8"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "platform_system == 'Windows'" },
{ name = "colorama", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593 }
sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593, upload-time = "2024-12-21T18:38:44.339Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188 },
{ url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188, upload-time = "2024-12-21T18:38:41.666Z" },
]
[[package]]
name = "colorama"
version = "0.4.6"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 }
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 },
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
]
[[package]]
name = "docker"
version = "7.1.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pywin32", marker = "sys_platform == 'win32'" },
{ name = "requests" },
{ name = "urllib3" },
]
sdist = { url = "https://files.pythonhosted.org/packages/91/9b/4a2ea29aeba62471211598dac5d96825bb49348fa07e906ea930394a83ce/docker-7.1.0.tar.gz", hash = "sha256:ad8c70e6e3f8926cb8a92619b832b4ea5299e2831c14284663184e200546fa6c", size = 117834 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e3/26/57c6fb270950d476074c087527a558ccb6f4436657314bfb6cdf484114c4/docker-7.1.0-py3-none-any.whl", hash = "sha256:c96b93b7f0a746f9e77d325bcfb87422a3d8bd4f03136ae8a85b37f1898d5fc0", size = 147774 },
]
[[package]]
name = "idna"
version = "3.10"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 },
]
[[package]]
name = "iniconfig"
version = "2.0.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 },
]
[[package]]
name = "markdown-it-py"
version = "3.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "mdurl" },
]
sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528 },
]
[[package]]
name = "mcontainer"
version = "0.1.0"
name = "cubbi"
version = "0.2.0"
source = { editable = "." }
dependencies = [
{ name = "docker" },
@@ -154,17 +111,62 @@ requires-dist = [
{ name = "ruff", marker = "extra == 'dev'", specifier = ">=0.1.9" },
{ name = "typer", specifier = ">=0.9.0" },
]
provides-extras = ["dev"]
[package.metadata.requires-dev]
dev = [{ name = "pytest", specifier = ">=8.3.5" }]
[[package]]
name = "docker"
version = "7.1.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pywin32", marker = "sys_platform == 'win32'" },
{ name = "requests" },
{ name = "urllib3" },
]
sdist = { url = "https://files.pythonhosted.org/packages/91/9b/4a2ea29aeba62471211598dac5d96825bb49348fa07e906ea930394a83ce/docker-7.1.0.tar.gz", hash = "sha256:ad8c70e6e3f8926cb8a92619b832b4ea5299e2831c14284663184e200546fa6c", size = 117834, upload-time = "2024-05-23T11:13:57.216Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e3/26/57c6fb270950d476074c087527a558ccb6f4436657314bfb6cdf484114c4/docker-7.1.0-py3-none-any.whl", hash = "sha256:c96b93b7f0a746f9e77d325bcfb87422a3d8bd4f03136ae8a85b37f1898d5fc0", size = 147774, upload-time = "2024-05-23T11:13:55.01Z" },
]
[[package]]
name = "idna"
version = "3.10"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" },
]
[[package]]
name = "iniconfig"
version = "2.0.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646, upload-time = "2023-01-07T11:08:11.254Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892, upload-time = "2023-01-07T11:08:09.864Z" },
]
[[package]]
name = "markdown-it-py"
version = "3.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "mdurl" },
]
sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596, upload-time = "2023-06-03T06:41:14.443Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528, upload-time = "2023-06-03T06:41:11.019Z" },
]
[[package]]
name = "mdurl"
version = "0.1.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729 }
sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 },
{ url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" },
]
[[package]]
@@ -175,48 +177,48 @@ dependencies = [
{ name = "mypy-extensions" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/ce/43/d5e49a86afa64bd3839ea0d5b9c7103487007d728e1293f52525d6d5486a/mypy-1.15.0.tar.gz", hash = "sha256:404534629d51d3efea5c800ee7c42b72a6554d6c400e6a79eafe15d11341fd43", size = 3239717 }
sdist = { url = "https://files.pythonhosted.org/packages/ce/43/d5e49a86afa64bd3839ea0d5b9c7103487007d728e1293f52525d6d5486a/mypy-1.15.0.tar.gz", hash = "sha256:404534629d51d3efea5c800ee7c42b72a6554d6c400e6a79eafe15d11341fd43", size = 3239717, upload-time = "2025-02-05T03:50:34.655Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/98/3a/03c74331c5eb8bd025734e04c9840532226775c47a2c39b56a0c8d4f128d/mypy-1.15.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:aea39e0583d05124836ea645f412e88a5c7d0fd77a6d694b60d9b6b2d9f184fd", size = 10793981 },
{ url = "https://files.pythonhosted.org/packages/f0/1a/41759b18f2cfd568848a37c89030aeb03534411eef981df621d8fad08a1d/mypy-1.15.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2f2147ab812b75e5b5499b01ade1f4a81489a147c01585cda36019102538615f", size = 9749175 },
{ url = "https://files.pythonhosted.org/packages/12/7e/873481abf1ef112c582db832740f4c11b2bfa510e829d6da29b0ab8c3f9c/mypy-1.15.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ce436f4c6d218a070048ed6a44c0bbb10cd2cc5e272b29e7845f6a2f57ee4464", size = 11455675 },
{ url = "https://files.pythonhosted.org/packages/b3/d0/92ae4cde706923a2d3f2d6c39629134063ff64b9dedca9c1388363da072d/mypy-1.15.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8023ff13985661b50a5928fc7a5ca15f3d1affb41e5f0a9952cb68ef090b31ee", size = 12410020 },
{ url = "https://files.pythonhosted.org/packages/46/8b/df49974b337cce35f828ba6fda228152d6db45fed4c86ba56ffe442434fd/mypy-1.15.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1124a18bc11a6a62887e3e137f37f53fbae476dc36c185d549d4f837a2a6a14e", size = 12498582 },
{ url = "https://files.pythonhosted.org/packages/13/50/da5203fcf6c53044a0b699939f31075c45ae8a4cadf538a9069b165c1050/mypy-1.15.0-cp312-cp312-win_amd64.whl", hash = "sha256:171a9ca9a40cd1843abeca0e405bc1940cd9b305eaeea2dda769ba096932bb22", size = 9366614 },
{ url = "https://files.pythonhosted.org/packages/6a/9b/fd2e05d6ffff24d912f150b87db9e364fa8282045c875654ce7e32fffa66/mypy-1.15.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:93faf3fdb04768d44bf28693293f3904bbb555d076b781ad2530214ee53e3445", size = 10788592 },
{ url = "https://files.pythonhosted.org/packages/74/37/b246d711c28a03ead1fd906bbc7106659aed7c089d55fe40dd58db812628/mypy-1.15.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:811aeccadfb730024c5d3e326b2fbe9249bb7413553f15499a4050f7c30e801d", size = 9753611 },
{ url = "https://files.pythonhosted.org/packages/a6/ac/395808a92e10cfdac8003c3de9a2ab6dc7cde6c0d2a4df3df1b815ffd067/mypy-1.15.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:98b7b9b9aedb65fe628c62a6dc57f6d5088ef2dfca37903a7d9ee374d03acca5", size = 11438443 },
{ url = "https://files.pythonhosted.org/packages/d2/8b/801aa06445d2de3895f59e476f38f3f8d610ef5d6908245f07d002676cbf/mypy-1.15.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c43a7682e24b4f576d93072216bf56eeff70d9140241f9edec0c104d0c515036", size = 12402541 },
{ url = "https://files.pythonhosted.org/packages/c7/67/5a4268782eb77344cc613a4cf23540928e41f018a9a1ec4c6882baf20ab8/mypy-1.15.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:baefc32840a9f00babd83251560e0ae1573e2f9d1b067719479bfb0e987c6357", size = 12494348 },
{ url = "https://files.pythonhosted.org/packages/83/3e/57bb447f7bbbfaabf1712d96f9df142624a386d98fb026a761532526057e/mypy-1.15.0-cp313-cp313-win_amd64.whl", hash = "sha256:b9378e2c00146c44793c98b8d5a61039a048e31f429fb0eb546d93f4b000bedf", size = 9373648 },
{ url = "https://files.pythonhosted.org/packages/09/4e/a7d65c7322c510de2c409ff3828b03354a7c43f5a8ed458a7a131b41c7b9/mypy-1.15.0-py3-none-any.whl", hash = "sha256:5469affef548bd1895d86d3bf10ce2b44e33d86923c29e4d675b3e323437ea3e", size = 2221777 },
{ url = "https://files.pythonhosted.org/packages/98/3a/03c74331c5eb8bd025734e04c9840532226775c47a2c39b56a0c8d4f128d/mypy-1.15.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:aea39e0583d05124836ea645f412e88a5c7d0fd77a6d694b60d9b6b2d9f184fd", size = 10793981, upload-time = "2025-02-05T03:50:28.25Z" },
{ url = "https://files.pythonhosted.org/packages/f0/1a/41759b18f2cfd568848a37c89030aeb03534411eef981df621d8fad08a1d/mypy-1.15.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2f2147ab812b75e5b5499b01ade1f4a81489a147c01585cda36019102538615f", size = 9749175, upload-time = "2025-02-05T03:50:13.411Z" },
{ url = "https://files.pythonhosted.org/packages/12/7e/873481abf1ef112c582db832740f4c11b2bfa510e829d6da29b0ab8c3f9c/mypy-1.15.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ce436f4c6d218a070048ed6a44c0bbb10cd2cc5e272b29e7845f6a2f57ee4464", size = 11455675, upload-time = "2025-02-05T03:50:31.421Z" },
{ url = "https://files.pythonhosted.org/packages/b3/d0/92ae4cde706923a2d3f2d6c39629134063ff64b9dedca9c1388363da072d/mypy-1.15.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8023ff13985661b50a5928fc7a5ca15f3d1affb41e5f0a9952cb68ef090b31ee", size = 12410020, upload-time = "2025-02-05T03:48:48.705Z" },
{ url = "https://files.pythonhosted.org/packages/46/8b/df49974b337cce35f828ba6fda228152d6db45fed4c86ba56ffe442434fd/mypy-1.15.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1124a18bc11a6a62887e3e137f37f53fbae476dc36c185d549d4f837a2a6a14e", size = 12498582, upload-time = "2025-02-05T03:49:03.628Z" },
{ url = "https://files.pythonhosted.org/packages/13/50/da5203fcf6c53044a0b699939f31075c45ae8a4cadf538a9069b165c1050/mypy-1.15.0-cp312-cp312-win_amd64.whl", hash = "sha256:171a9ca9a40cd1843abeca0e405bc1940cd9b305eaeea2dda769ba096932bb22", size = 9366614, upload-time = "2025-02-05T03:50:00.313Z" },
{ url = "https://files.pythonhosted.org/packages/6a/9b/fd2e05d6ffff24d912f150b87db9e364fa8282045c875654ce7e32fffa66/mypy-1.15.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:93faf3fdb04768d44bf28693293f3904bbb555d076b781ad2530214ee53e3445", size = 10788592, upload-time = "2025-02-05T03:48:55.789Z" },
{ url = "https://files.pythonhosted.org/packages/74/37/b246d711c28a03ead1fd906bbc7106659aed7c089d55fe40dd58db812628/mypy-1.15.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:811aeccadfb730024c5d3e326b2fbe9249bb7413553f15499a4050f7c30e801d", size = 9753611, upload-time = "2025-02-05T03:48:44.581Z" },
{ url = "https://files.pythonhosted.org/packages/a6/ac/395808a92e10cfdac8003c3de9a2ab6dc7cde6c0d2a4df3df1b815ffd067/mypy-1.15.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:98b7b9b9aedb65fe628c62a6dc57f6d5088ef2dfca37903a7d9ee374d03acca5", size = 11438443, upload-time = "2025-02-05T03:49:25.514Z" },
{ url = "https://files.pythonhosted.org/packages/d2/8b/801aa06445d2de3895f59e476f38f3f8d610ef5d6908245f07d002676cbf/mypy-1.15.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c43a7682e24b4f576d93072216bf56eeff70d9140241f9edec0c104d0c515036", size = 12402541, upload-time = "2025-02-05T03:49:57.623Z" },
{ url = "https://files.pythonhosted.org/packages/c7/67/5a4268782eb77344cc613a4cf23540928e41f018a9a1ec4c6882baf20ab8/mypy-1.15.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:baefc32840a9f00babd83251560e0ae1573e2f9d1b067719479bfb0e987c6357", size = 12494348, upload-time = "2025-02-05T03:48:52.361Z" },
{ url = "https://files.pythonhosted.org/packages/83/3e/57bb447f7bbbfaabf1712d96f9df142624a386d98fb026a761532526057e/mypy-1.15.0-cp313-cp313-win_amd64.whl", hash = "sha256:b9378e2c00146c44793c98b8d5a61039a048e31f429fb0eb546d93f4b000bedf", size = 9373648, upload-time = "2025-02-05T03:49:11.395Z" },
{ url = "https://files.pythonhosted.org/packages/09/4e/a7d65c7322c510de2c409ff3828b03354a7c43f5a8ed458a7a131b41c7b9/mypy-1.15.0-py3-none-any.whl", hash = "sha256:5469affef548bd1895d86d3bf10ce2b44e33d86923c29e4d675b3e323437ea3e", size = 2221777, upload-time = "2025-02-05T03:50:08.348Z" },
]
[[package]]
name = "mypy-extensions"
version = "1.0.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/98/a4/1ab47638b92648243faf97a5aeb6ea83059cc3624972ab6b8d2316078d3f/mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782", size = 4433 }
sdist = { url = "https://files.pythonhosted.org/packages/98/a4/1ab47638b92648243faf97a5aeb6ea83059cc3624972ab6b8d2316078d3f/mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782", size = 4433, upload-time = "2023-02-04T12:11:27.157Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2a/e2/5d3f6ada4297caebe1a2add3b126fe800c96f56dbe5d1988a2cbe0b267aa/mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", size = 4695 },
{ url = "https://files.pythonhosted.org/packages/2a/e2/5d3f6ada4297caebe1a2add3b126fe800c96f56dbe5d1988a2cbe0b267aa/mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", size = 4695, upload-time = "2023-02-04T12:11:25.002Z" },
]
[[package]]
name = "packaging"
version = "24.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950 }
sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950, upload-time = "2024-11-08T09:47:47.202Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 },
{ url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451, upload-time = "2024-11-08T09:47:44.722Z" },
]
[[package]]
name = "pluggy"
version = "1.5.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955 }
sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955, upload-time = "2024-04-20T21:34:42.531Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 },
{ url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556, upload-time = "2024-04-20T21:34:40.434Z" },
]
[[package]]
@@ -228,9 +230,9 @@ dependencies = [
{ name = "pydantic-core" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/b7/ae/d5220c5c52b158b1de7ca89fc5edb72f304a70a4c540c84c8844bf4008de/pydantic-2.10.6.tar.gz", hash = "sha256:ca5daa827cce33de7a42be142548b0096bf05a7e7b365aebfa5f8eeec7128236", size = 761681 }
sdist = { url = "https://files.pythonhosted.org/packages/b7/ae/d5220c5c52b158b1de7ca89fc5edb72f304a70a4c540c84c8844bf4008de/pydantic-2.10.6.tar.gz", hash = "sha256:ca5daa827cce33de7a42be142548b0096bf05a7e7b365aebfa5f8eeec7128236", size = 761681, upload-time = "2025-01-24T01:42:12.693Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f4/3c/8cc1cc84deffa6e25d2d0c688ebb80635dfdbf1dbea3e30c541c8cf4d860/pydantic-2.10.6-py3-none-any.whl", hash = "sha256:427d664bf0b8a2b34ff5dd0f5a18df00591adcee7198fbd71981054cef37b584", size = 431696 },
{ url = "https://files.pythonhosted.org/packages/f4/3c/8cc1cc84deffa6e25d2d0c688ebb80635dfdbf1dbea3e30c541c8cf4d860/pydantic-2.10.6-py3-none-any.whl", hash = "sha256:427d664bf0b8a2b34ff5dd0f5a18df00591adcee7198fbd71981054cef37b584", size = 431696, upload-time = "2025-01-24T01:42:10.371Z" },
]
[[package]]
@@ -240,45 +242,45 @@ source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/fc/01/f3e5ac5e7c25833db5eb555f7b7ab24cd6f8c322d3a3ad2d67a952dc0abc/pydantic_core-2.27.2.tar.gz", hash = "sha256:eb026e5a4c1fee05726072337ff51d1efb6f59090b7da90d30ea58625b1ffb39", size = 413443 }
sdist = { url = "https://files.pythonhosted.org/packages/fc/01/f3e5ac5e7c25833db5eb555f7b7ab24cd6f8c322d3a3ad2d67a952dc0abc/pydantic_core-2.27.2.tar.gz", hash = "sha256:eb026e5a4c1fee05726072337ff51d1efb6f59090b7da90d30ea58625b1ffb39", size = 413443, upload-time = "2024-12-18T11:31:54.917Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d6/74/51c8a5482ca447871c93e142d9d4a92ead74de6c8dc5e66733e22c9bba89/pydantic_core-2.27.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9e0c8cfefa0ef83b4da9588448b6d8d2a2bf1a53c3f1ae5fca39eb3061e2f0b0", size = 1893127 },
{ url = "https://files.pythonhosted.org/packages/d3/f3/c97e80721735868313c58b89d2de85fa80fe8dfeeed84dc51598b92a135e/pydantic_core-2.27.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:83097677b8e3bd7eaa6775720ec8e0405f1575015a463285a92bfdfe254529ef", size = 1811340 },
{ url = "https://files.pythonhosted.org/packages/9e/91/840ec1375e686dbae1bd80a9e46c26a1e0083e1186abc610efa3d9a36180/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:172fce187655fece0c90d90a678424b013f8fbb0ca8b036ac266749c09438cb7", size = 1822900 },
{ url = "https://files.pythonhosted.org/packages/f6/31/4240bc96025035500c18adc149aa6ffdf1a0062a4b525c932065ceb4d868/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:519f29f5213271eeeeb3093f662ba2fd512b91c5f188f3bb7b27bc5973816934", size = 1869177 },
{ url = "https://files.pythonhosted.org/packages/fa/20/02fbaadb7808be578317015c462655c317a77a7c8f0ef274bc016a784c54/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05e3a55d124407fffba0dd6b0c0cd056d10e983ceb4e5dbd10dda135c31071d6", size = 2038046 },
{ url = "https://files.pythonhosted.org/packages/06/86/7f306b904e6c9eccf0668248b3f272090e49c275bc488a7b88b0823444a4/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c3ed807c7b91de05e63930188f19e921d1fe90de6b4f5cd43ee7fcc3525cb8c", size = 2685386 },
{ url = "https://files.pythonhosted.org/packages/8d/f0/49129b27c43396581a635d8710dae54a791b17dfc50c70164866bbf865e3/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fb4aadc0b9a0c063206846d603b92030eb6f03069151a625667f982887153e2", size = 1997060 },
{ url = "https://files.pythonhosted.org/packages/0d/0f/943b4af7cd416c477fd40b187036c4f89b416a33d3cc0ab7b82708a667aa/pydantic_core-2.27.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28ccb213807e037460326424ceb8b5245acb88f32f3d2777427476e1b32c48c4", size = 2004870 },
{ url = "https://files.pythonhosted.org/packages/35/40/aea70b5b1a63911c53a4c8117c0a828d6790483f858041f47bab0b779f44/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:de3cd1899e2c279b140adde9357c4495ed9d47131b4a4eaff9052f23398076b3", size = 1999822 },
{ url = "https://files.pythonhosted.org/packages/f2/b3/807b94fd337d58effc5498fd1a7a4d9d59af4133e83e32ae39a96fddec9d/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:220f892729375e2d736b97d0e51466252ad84c51857d4d15f5e9692f9ef12be4", size = 2130364 },
{ url = "https://files.pythonhosted.org/packages/fc/df/791c827cd4ee6efd59248dca9369fb35e80a9484462c33c6649a8d02b565/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a0fcd29cd6b4e74fe8ddd2c90330fd8edf2e30cb52acda47f06dd615ae72da57", size = 2158303 },
{ url = "https://files.pythonhosted.org/packages/9b/67/4e197c300976af185b7cef4c02203e175fb127e414125916bf1128b639a9/pydantic_core-2.27.2-cp312-cp312-win32.whl", hash = "sha256:1e2cb691ed9834cd6a8be61228471d0a503731abfb42f82458ff27be7b2186fc", size = 1834064 },
{ url = "https://files.pythonhosted.org/packages/1f/ea/cd7209a889163b8dcca139fe32b9687dd05249161a3edda62860430457a5/pydantic_core-2.27.2-cp312-cp312-win_amd64.whl", hash = "sha256:cc3f1a99a4f4f9dd1de4fe0312c114e740b5ddead65bb4102884b384c15d8bc9", size = 1989046 },
{ url = "https://files.pythonhosted.org/packages/bc/49/c54baab2f4658c26ac633d798dab66b4c3a9bbf47cff5284e9c182f4137a/pydantic_core-2.27.2-cp312-cp312-win_arm64.whl", hash = "sha256:3911ac9284cd8a1792d3cb26a2da18f3ca26c6908cc434a18f730dc0db7bfa3b", size = 1885092 },
{ url = "https://files.pythonhosted.org/packages/41/b1/9bc383f48f8002f99104e3acff6cba1231b29ef76cfa45d1506a5cad1f84/pydantic_core-2.27.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7d14bd329640e63852364c306f4d23eb744e0f8193148d4044dd3dacdaacbd8b", size = 1892709 },
{ url = "https://files.pythonhosted.org/packages/10/6c/e62b8657b834f3eb2961b49ec8e301eb99946245e70bf42c8817350cbefc/pydantic_core-2.27.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82f91663004eb8ed30ff478d77c4d1179b3563df6cdb15c0817cd1cdaf34d154", size = 1811273 },
{ url = "https://files.pythonhosted.org/packages/ba/15/52cfe49c8c986e081b863b102d6b859d9defc63446b642ccbbb3742bf371/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71b24c7d61131bb83df10cc7e687433609963a944ccf45190cfc21e0887b08c9", size = 1823027 },
{ url = "https://files.pythonhosted.org/packages/b1/1c/b6f402cfc18ec0024120602bdbcebc7bdd5b856528c013bd4d13865ca473/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fa8e459d4954f608fa26116118bb67f56b93b209c39b008277ace29937453dc9", size = 1868888 },
{ url = "https://files.pythonhosted.org/packages/bd/7b/8cb75b66ac37bc2975a3b7de99f3c6f355fcc4d89820b61dffa8f1e81677/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce8918cbebc8da707ba805b7fd0b382816858728ae7fe19a942080c24e5b7cd1", size = 2037738 },
{ url = "https://files.pythonhosted.org/packages/c8/f1/786d8fe78970a06f61df22cba58e365ce304bf9b9f46cc71c8c424e0c334/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eda3f5c2a021bbc5d976107bb302e0131351c2ba54343f8a496dc8783d3d3a6a", size = 2685138 },
{ url = "https://files.pythonhosted.org/packages/a6/74/d12b2cd841d8724dc8ffb13fc5cef86566a53ed358103150209ecd5d1999/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd8086fa684c4775c27f03f062cbb9eaa6e17f064307e86b21b9e0abc9c0f02e", size = 1997025 },
{ url = "https://files.pythonhosted.org/packages/a0/6e/940bcd631bc4d9a06c9539b51f070b66e8f370ed0933f392db6ff350d873/pydantic_core-2.27.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8d9b3388db186ba0c099a6d20f0604a44eabdeef1777ddd94786cdae158729e4", size = 2004633 },
{ url = "https://files.pythonhosted.org/packages/50/cc/a46b34f1708d82498c227d5d80ce615b2dd502ddcfd8376fc14a36655af1/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7a66efda2387de898c8f38c0cf7f14fca0b51a8ef0b24bfea5849f1b3c95af27", size = 1999404 },
{ url = "https://files.pythonhosted.org/packages/ca/2d/c365cfa930ed23bc58c41463bae347d1005537dc8db79e998af8ba28d35e/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:18a101c168e4e092ab40dbc2503bdc0f62010e95d292b27827871dc85450d7ee", size = 2130130 },
{ url = "https://files.pythonhosted.org/packages/f4/d7/eb64d015c350b7cdb371145b54d96c919d4db516817f31cd1c650cae3b21/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ba5dd002f88b78a4215ed2f8ddbdf85e8513382820ba15ad5ad8955ce0ca19a1", size = 2157946 },
{ url = "https://files.pythonhosted.org/packages/a4/99/bddde3ddde76c03b65dfd5a66ab436c4e58ffc42927d4ff1198ffbf96f5f/pydantic_core-2.27.2-cp313-cp313-win32.whl", hash = "sha256:1ebaf1d0481914d004a573394f4be3a7616334be70261007e47c2a6fe7e50130", size = 1834387 },
{ url = "https://files.pythonhosted.org/packages/71/47/82b5e846e01b26ac6f1893d3c5f9f3a2eb6ba79be26eef0b759b4fe72946/pydantic_core-2.27.2-cp313-cp313-win_amd64.whl", hash = "sha256:953101387ecf2f5652883208769a79e48db18c6df442568a0b5ccd8c2723abee", size = 1990453 },
{ url = "https://files.pythonhosted.org/packages/51/b2/b2b50d5ecf21acf870190ae5d093602d95f66c9c31f9d5de6062eb329ad1/pydantic_core-2.27.2-cp313-cp313-win_arm64.whl", hash = "sha256:ac4dbfd1691affb8f48c2c13241a2e3b60ff23247cbcf981759c768b6633cf8b", size = 1885186 },
{ url = "https://files.pythonhosted.org/packages/d6/74/51c8a5482ca447871c93e142d9d4a92ead74de6c8dc5e66733e22c9bba89/pydantic_core-2.27.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9e0c8cfefa0ef83b4da9588448b6d8d2a2bf1a53c3f1ae5fca39eb3061e2f0b0", size = 1893127, upload-time = "2024-12-18T11:28:30.346Z" },
{ url = "https://files.pythonhosted.org/packages/d3/f3/c97e80721735868313c58b89d2de85fa80fe8dfeeed84dc51598b92a135e/pydantic_core-2.27.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:83097677b8e3bd7eaa6775720ec8e0405f1575015a463285a92bfdfe254529ef", size = 1811340, upload-time = "2024-12-18T11:28:32.521Z" },
{ url = "https://files.pythonhosted.org/packages/9e/91/840ec1375e686dbae1bd80a9e46c26a1e0083e1186abc610efa3d9a36180/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:172fce187655fece0c90d90a678424b013f8fbb0ca8b036ac266749c09438cb7", size = 1822900, upload-time = "2024-12-18T11:28:34.507Z" },
{ url = "https://files.pythonhosted.org/packages/f6/31/4240bc96025035500c18adc149aa6ffdf1a0062a4b525c932065ceb4d868/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:519f29f5213271eeeeb3093f662ba2fd512b91c5f188f3bb7b27bc5973816934", size = 1869177, upload-time = "2024-12-18T11:28:36.488Z" },
{ url = "https://files.pythonhosted.org/packages/fa/20/02fbaadb7808be578317015c462655c317a77a7c8f0ef274bc016a784c54/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05e3a55d124407fffba0dd6b0c0cd056d10e983ceb4e5dbd10dda135c31071d6", size = 2038046, upload-time = "2024-12-18T11:28:39.409Z" },
{ url = "https://files.pythonhosted.org/packages/06/86/7f306b904e6c9eccf0668248b3f272090e49c275bc488a7b88b0823444a4/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c3ed807c7b91de05e63930188f19e921d1fe90de6b4f5cd43ee7fcc3525cb8c", size = 2685386, upload-time = "2024-12-18T11:28:41.221Z" },
{ url = "https://files.pythonhosted.org/packages/8d/f0/49129b27c43396581a635d8710dae54a791b17dfc50c70164866bbf865e3/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fb4aadc0b9a0c063206846d603b92030eb6f03069151a625667f982887153e2", size = 1997060, upload-time = "2024-12-18T11:28:44.709Z" },
{ url = "https://files.pythonhosted.org/packages/0d/0f/943b4af7cd416c477fd40b187036c4f89b416a33d3cc0ab7b82708a667aa/pydantic_core-2.27.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28ccb213807e037460326424ceb8b5245acb88f32f3d2777427476e1b32c48c4", size = 2004870, upload-time = "2024-12-18T11:28:46.839Z" },
{ url = "https://files.pythonhosted.org/packages/35/40/aea70b5b1a63911c53a4c8117c0a828d6790483f858041f47bab0b779f44/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:de3cd1899e2c279b140adde9357c4495ed9d47131b4a4eaff9052f23398076b3", size = 1999822, upload-time = "2024-12-18T11:28:48.896Z" },
{ url = "https://files.pythonhosted.org/packages/f2/b3/807b94fd337d58effc5498fd1a7a4d9d59af4133e83e32ae39a96fddec9d/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:220f892729375e2d736b97d0e51466252ad84c51857d4d15f5e9692f9ef12be4", size = 2130364, upload-time = "2024-12-18T11:28:50.755Z" },
{ url = "https://files.pythonhosted.org/packages/fc/df/791c827cd4ee6efd59248dca9369fb35e80a9484462c33c6649a8d02b565/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a0fcd29cd6b4e74fe8ddd2c90330fd8edf2e30cb52acda47f06dd615ae72da57", size = 2158303, upload-time = "2024-12-18T11:28:54.122Z" },
{ url = "https://files.pythonhosted.org/packages/9b/67/4e197c300976af185b7cef4c02203e175fb127e414125916bf1128b639a9/pydantic_core-2.27.2-cp312-cp312-win32.whl", hash = "sha256:1e2cb691ed9834cd6a8be61228471d0a503731abfb42f82458ff27be7b2186fc", size = 1834064, upload-time = "2024-12-18T11:28:56.074Z" },
{ url = "https://files.pythonhosted.org/packages/1f/ea/cd7209a889163b8dcca139fe32b9687dd05249161a3edda62860430457a5/pydantic_core-2.27.2-cp312-cp312-win_amd64.whl", hash = "sha256:cc3f1a99a4f4f9dd1de4fe0312c114e740b5ddead65bb4102884b384c15d8bc9", size = 1989046, upload-time = "2024-12-18T11:28:58.107Z" },
{ url = "https://files.pythonhosted.org/packages/bc/49/c54baab2f4658c26ac633d798dab66b4c3a9bbf47cff5284e9c182f4137a/pydantic_core-2.27.2-cp312-cp312-win_arm64.whl", hash = "sha256:3911ac9284cd8a1792d3cb26a2da18f3ca26c6908cc434a18f730dc0db7bfa3b", size = 1885092, upload-time = "2024-12-18T11:29:01.335Z" },
{ url = "https://files.pythonhosted.org/packages/41/b1/9bc383f48f8002f99104e3acff6cba1231b29ef76cfa45d1506a5cad1f84/pydantic_core-2.27.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7d14bd329640e63852364c306f4d23eb744e0f8193148d4044dd3dacdaacbd8b", size = 1892709, upload-time = "2024-12-18T11:29:03.193Z" },
{ url = "https://files.pythonhosted.org/packages/10/6c/e62b8657b834f3eb2961b49ec8e301eb99946245e70bf42c8817350cbefc/pydantic_core-2.27.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82f91663004eb8ed30ff478d77c4d1179b3563df6cdb15c0817cd1cdaf34d154", size = 1811273, upload-time = "2024-12-18T11:29:05.306Z" },
{ url = "https://files.pythonhosted.org/packages/ba/15/52cfe49c8c986e081b863b102d6b859d9defc63446b642ccbbb3742bf371/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71b24c7d61131bb83df10cc7e687433609963a944ccf45190cfc21e0887b08c9", size = 1823027, upload-time = "2024-12-18T11:29:07.294Z" },
{ url = "https://files.pythonhosted.org/packages/b1/1c/b6f402cfc18ec0024120602bdbcebc7bdd5b856528c013bd4d13865ca473/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fa8e459d4954f608fa26116118bb67f56b93b209c39b008277ace29937453dc9", size = 1868888, upload-time = "2024-12-18T11:29:09.249Z" },
{ url = "https://files.pythonhosted.org/packages/bd/7b/8cb75b66ac37bc2975a3b7de99f3c6f355fcc4d89820b61dffa8f1e81677/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce8918cbebc8da707ba805b7fd0b382816858728ae7fe19a942080c24e5b7cd1", size = 2037738, upload-time = "2024-12-18T11:29:11.23Z" },
{ url = "https://files.pythonhosted.org/packages/c8/f1/786d8fe78970a06f61df22cba58e365ce304bf9b9f46cc71c8c424e0c334/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eda3f5c2a021bbc5d976107bb302e0131351c2ba54343f8a496dc8783d3d3a6a", size = 2685138, upload-time = "2024-12-18T11:29:16.396Z" },
{ url = "https://files.pythonhosted.org/packages/a6/74/d12b2cd841d8724dc8ffb13fc5cef86566a53ed358103150209ecd5d1999/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd8086fa684c4775c27f03f062cbb9eaa6e17f064307e86b21b9e0abc9c0f02e", size = 1997025, upload-time = "2024-12-18T11:29:20.25Z" },
{ url = "https://files.pythonhosted.org/packages/a0/6e/940bcd631bc4d9a06c9539b51f070b66e8f370ed0933f392db6ff350d873/pydantic_core-2.27.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8d9b3388db186ba0c099a6d20f0604a44eabdeef1777ddd94786cdae158729e4", size = 2004633, upload-time = "2024-12-18T11:29:23.877Z" },
{ url = "https://files.pythonhosted.org/packages/50/cc/a46b34f1708d82498c227d5d80ce615b2dd502ddcfd8376fc14a36655af1/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7a66efda2387de898c8f38c0cf7f14fca0b51a8ef0b24bfea5849f1b3c95af27", size = 1999404, upload-time = "2024-12-18T11:29:25.872Z" },
{ url = "https://files.pythonhosted.org/packages/ca/2d/c365cfa930ed23bc58c41463bae347d1005537dc8db79e998af8ba28d35e/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:18a101c168e4e092ab40dbc2503bdc0f62010e95d292b27827871dc85450d7ee", size = 2130130, upload-time = "2024-12-18T11:29:29.252Z" },
{ url = "https://files.pythonhosted.org/packages/f4/d7/eb64d015c350b7cdb371145b54d96c919d4db516817f31cd1c650cae3b21/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ba5dd002f88b78a4215ed2f8ddbdf85e8513382820ba15ad5ad8955ce0ca19a1", size = 2157946, upload-time = "2024-12-18T11:29:31.338Z" },
{ url = "https://files.pythonhosted.org/packages/a4/99/bddde3ddde76c03b65dfd5a66ab436c4e58ffc42927d4ff1198ffbf96f5f/pydantic_core-2.27.2-cp313-cp313-win32.whl", hash = "sha256:1ebaf1d0481914d004a573394f4be3a7616334be70261007e47c2a6fe7e50130", size = 1834387, upload-time = "2024-12-18T11:29:33.481Z" },
{ url = "https://files.pythonhosted.org/packages/71/47/82b5e846e01b26ac6f1893d3c5f9f3a2eb6ba79be26eef0b759b4fe72946/pydantic_core-2.27.2-cp313-cp313-win_amd64.whl", hash = "sha256:953101387ecf2f5652883208769a79e48db18c6df442568a0b5ccd8c2723abee", size = 1990453, upload-time = "2024-12-18T11:29:35.533Z" },
{ url = "https://files.pythonhosted.org/packages/51/b2/b2b50d5ecf21acf870190ae5d093602d95f66c9c31f9d5de6062eb329ad1/pydantic_core-2.27.2-cp313-cp313-win_arm64.whl", hash = "sha256:ac4dbfd1691affb8f48c2c13241a2e3b60ff23247cbcf981759c768b6633cf8b", size = 1885186, upload-time = "2024-12-18T11:29:37.649Z" },
]
[[package]]
name = "pygments"
version = "2.19.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581 }
sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581, upload-time = "2025-01-06T17:26:30.443Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293 },
{ url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293, upload-time = "2025-01-06T17:26:25.553Z" },
]
[[package]]
@@ -291,9 +293,9 @@ dependencies = [
{ name = "packaging" },
{ name = "pluggy" },
]
sdist = { url = "https://files.pythonhosted.org/packages/ae/3c/c9d525a414d506893f0cd8a8d0de7706446213181570cdbd766691164e40/pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845", size = 1450891 }
sdist = { url = "https://files.pythonhosted.org/packages/ae/3c/c9d525a414d506893f0cd8a8d0de7706446213181570cdbd766691164e40/pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845", size = 1450891, upload-time = "2025-03-02T12:54:54.503Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634 },
{ url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634, upload-time = "2025-03-02T12:54:52.069Z" },
]
[[package]]
@@ -301,38 +303,38 @@ name = "pywin32"
version = "309"
source = { registry = "https://pypi.org/simple" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/20/2c/b0240b14ff3dba7a8a7122dc9bbf7fbd21ed0e8b57c109633675b5d1761f/pywin32-309-cp312-cp312-win32.whl", hash = "sha256:de9acacced5fa82f557298b1fed5fef7bd49beee04190f68e1e4783fbdc19926", size = 8790648 },
{ url = "https://files.pythonhosted.org/packages/dd/11/c36884c732e2b3397deee808b5dac1abbb170ec37f94c6606fcb04d1e9d7/pywin32-309-cp312-cp312-win_amd64.whl", hash = "sha256:6ff9eebb77ffc3d59812c68db33c0a7817e1337e3537859499bd27586330fc9e", size = 9497399 },
{ url = "https://files.pythonhosted.org/packages/18/9f/79703972958f8ba3fd38bc9bf1165810bd75124982419b0cc433a2894d46/pywin32-309-cp312-cp312-win_arm64.whl", hash = "sha256:619f3e0a327b5418d833f44dc87859523635cf339f86071cc65a13c07be3110f", size = 8454122 },
{ url = "https://files.pythonhosted.org/packages/6c/c3/51aca6887cc5e410aa4cdc55662cf8438212440c67335c3f141b02eb8d52/pywin32-309-cp313-cp313-win32.whl", hash = "sha256:008bffd4afd6de8ca46c6486085414cc898263a21a63c7f860d54c9d02b45c8d", size = 8789700 },
{ url = "https://files.pythonhosted.org/packages/dd/66/330f265140fa814b4ed1bf16aea701f9d005f8f4ab57a54feb17f53afe7e/pywin32-309-cp313-cp313-win_amd64.whl", hash = "sha256:bd0724f58492db4cbfbeb1fcd606495205aa119370c0ddc4f70e5771a3ab768d", size = 9496714 },
{ url = "https://files.pythonhosted.org/packages/2c/84/9a51e6949a03f25cd329ece54dbf0846d57fadd2e79046c3b8d140aaa132/pywin32-309-cp313-cp313-win_arm64.whl", hash = "sha256:8fd9669cfd41863b688a1bc9b1d4d2d76fd4ba2128be50a70b0ea66b8d37953b", size = 8453052 },
{ url = "https://files.pythonhosted.org/packages/20/2c/b0240b14ff3dba7a8a7122dc9bbf7fbd21ed0e8b57c109633675b5d1761f/pywin32-309-cp312-cp312-win32.whl", hash = "sha256:de9acacced5fa82f557298b1fed5fef7bd49beee04190f68e1e4783fbdc19926", size = 8790648, upload-time = "2025-03-09T18:04:03.253Z" },
{ url = "https://files.pythonhosted.org/packages/dd/11/c36884c732e2b3397deee808b5dac1abbb170ec37f94c6606fcb04d1e9d7/pywin32-309-cp312-cp312-win_amd64.whl", hash = "sha256:6ff9eebb77ffc3d59812c68db33c0a7817e1337e3537859499bd27586330fc9e", size = 9497399, upload-time = "2025-03-09T18:04:05.388Z" },
{ url = "https://files.pythonhosted.org/packages/18/9f/79703972958f8ba3fd38bc9bf1165810bd75124982419b0cc433a2894d46/pywin32-309-cp312-cp312-win_arm64.whl", hash = "sha256:619f3e0a327b5418d833f44dc87859523635cf339f86071cc65a13c07be3110f", size = 8454122, upload-time = "2025-03-09T18:04:07.217Z" },
{ url = "https://files.pythonhosted.org/packages/6c/c3/51aca6887cc5e410aa4cdc55662cf8438212440c67335c3f141b02eb8d52/pywin32-309-cp313-cp313-win32.whl", hash = "sha256:008bffd4afd6de8ca46c6486085414cc898263a21a63c7f860d54c9d02b45c8d", size = 8789700, upload-time = "2025-03-09T18:04:08.937Z" },
{ url = "https://files.pythonhosted.org/packages/dd/66/330f265140fa814b4ed1bf16aea701f9d005f8f4ab57a54feb17f53afe7e/pywin32-309-cp313-cp313-win_amd64.whl", hash = "sha256:bd0724f58492db4cbfbeb1fcd606495205aa119370c0ddc4f70e5771a3ab768d", size = 9496714, upload-time = "2025-03-09T18:04:10.619Z" },
{ url = "https://files.pythonhosted.org/packages/2c/84/9a51e6949a03f25cd329ece54dbf0846d57fadd2e79046c3b8d140aaa132/pywin32-309-cp313-cp313-win_arm64.whl", hash = "sha256:8fd9669cfd41863b688a1bc9b1d4d2d76fd4ba2128be50a70b0ea66b8d37953b", size = 8453052, upload-time = "2025-03-09T18:04:12.812Z" },
]
[[package]]
name = "pyyaml"
version = "6.0.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631 }
sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631, upload-time = "2024-08-06T20:33:50.674Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873 },
{ url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302 },
{ url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154 },
{ url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223 },
{ url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542 },
{ url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164 },
{ url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611 },
{ url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591 },
{ url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338 },
{ url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309 },
{ url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679 },
{ url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428 },
{ url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361 },
{ url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523 },
{ url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660 },
{ url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597 },
{ url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527 },
{ url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446 },
{ url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873, upload-time = "2024-08-06T20:32:25.131Z" },
{ url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302, upload-time = "2024-08-06T20:32:26.511Z" },
{ url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154, upload-time = "2024-08-06T20:32:28.363Z" },
{ url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223, upload-time = "2024-08-06T20:32:30.058Z" },
{ url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542, upload-time = "2024-08-06T20:32:31.881Z" },
{ url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164, upload-time = "2024-08-06T20:32:37.083Z" },
{ url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611, upload-time = "2024-08-06T20:32:38.898Z" },
{ url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591, upload-time = "2024-08-06T20:32:40.241Z" },
{ url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338, upload-time = "2024-08-06T20:32:41.93Z" },
{ url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309, upload-time = "2024-08-06T20:32:43.4Z" },
{ url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679, upload-time = "2024-08-06T20:32:44.801Z" },
{ url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428, upload-time = "2024-08-06T20:32:46.432Z" },
{ url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361, upload-time = "2024-08-06T20:32:51.188Z" },
{ url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523, upload-time = "2024-08-06T20:32:53.019Z" },
{ url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660, upload-time = "2024-08-06T20:32:54.708Z" },
{ url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597, upload-time = "2024-08-06T20:32:56.985Z" },
{ url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527, upload-time = "2024-08-06T20:33:03.001Z" },
{ url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446, upload-time = "2024-08-06T20:33:04.33Z" },
]
[[package]]
@@ -345,9 +347,9 @@ dependencies = [
{ name = "idna" },
{ name = "urllib3" },
]
sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218 }
sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218, upload-time = "2024-05-29T15:37:49.536Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 },
{ url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928, upload-time = "2024-05-29T15:37:47.027Z" },
]
[[package]]
@@ -358,43 +360,43 @@ dependencies = [
{ name = "markdown-it-py" },
{ name = "pygments" },
]
sdist = { url = "https://files.pythonhosted.org/packages/ab/3a/0316b28d0761c6734d6bc14e770d85506c986c85ffb239e688eeaab2c2bc/rich-13.9.4.tar.gz", hash = "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098", size = 223149 }
sdist = { url = "https://files.pythonhosted.org/packages/ab/3a/0316b28d0761c6734d6bc14e770d85506c986c85ffb239e688eeaab2c2bc/rich-13.9.4.tar.gz", hash = "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098", size = 223149, upload-time = "2024-11-01T16:43:57.873Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/19/71/39c7c0d87f8d4e6c020a393182060eaefeeae6c01dab6a84ec346f2567df/rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90", size = 242424 },
{ url = "https://files.pythonhosted.org/packages/19/71/39c7c0d87f8d4e6c020a393182060eaefeeae6c01dab6a84ec346f2567df/rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90", size = 242424, upload-time = "2024-11-01T16:43:55.817Z" },
]
[[package]]
name = "ruff"
version = "0.9.10"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/20/8e/fafaa6f15c332e73425d9c44ada85360501045d5ab0b81400076aff27cf6/ruff-0.9.10.tar.gz", hash = "sha256:9bacb735d7bada9cfb0f2c227d3658fc443d90a727b47f206fb33f52f3c0eac7", size = 3759776 }
sdist = { url = "https://files.pythonhosted.org/packages/20/8e/fafaa6f15c332e73425d9c44ada85360501045d5ab0b81400076aff27cf6/ruff-0.9.10.tar.gz", hash = "sha256:9bacb735d7bada9cfb0f2c227d3658fc443d90a727b47f206fb33f52f3c0eac7", size = 3759776, upload-time = "2025-03-07T15:27:44.363Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/73/b2/af7c2cc9e438cbc19fafeec4f20bfcd72165460fe75b2b6e9a0958c8c62b/ruff-0.9.10-py3-none-linux_armv6l.whl", hash = "sha256:eb4d25532cfd9fe461acc83498361ec2e2252795b4f40b17e80692814329e42d", size = 10049494 },
{ url = "https://files.pythonhosted.org/packages/6d/12/03f6dfa1b95ddd47e6969f0225d60d9d7437c91938a310835feb27927ca0/ruff-0.9.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:188a6638dab1aa9bb6228a7302387b2c9954e455fb25d6b4470cb0641d16759d", size = 10853584 },
{ url = "https://files.pythonhosted.org/packages/02/49/1c79e0906b6ff551fb0894168763f705bf980864739572b2815ecd3c9df0/ruff-0.9.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:5284dcac6b9dbc2fcb71fdfc26a217b2ca4ede6ccd57476f52a587451ebe450d", size = 10155692 },
{ url = "https://files.pythonhosted.org/packages/5b/01/85e8082e41585e0e1ceb11e41c054e9e36fed45f4b210991052d8a75089f/ruff-0.9.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:47678f39fa2a3da62724851107f438c8229a3470f533894b5568a39b40029c0c", size = 10369760 },
{ url = "https://files.pythonhosted.org/packages/a1/90/0bc60bd4e5db051f12445046d0c85cc2c617095c0904f1aa81067dc64aea/ruff-0.9.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:99713a6e2766b7a17147b309e8c915b32b07a25c9efd12ada79f217c9c778b3e", size = 9912196 },
{ url = "https://files.pythonhosted.org/packages/66/ea/0b7e8c42b1ec608033c4d5a02939c82097ddcb0b3e393e4238584b7054ab/ruff-0.9.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:524ee184d92f7c7304aa568e2db20f50c32d1d0caa235d8ddf10497566ea1a12", size = 11434985 },
{ url = "https://files.pythonhosted.org/packages/d5/86/3171d1eff893db4f91755175a6e1163c5887be1f1e2f4f6c0c59527c2bfd/ruff-0.9.10-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:df92aeac30af821f9acf819fc01b4afc3dfb829d2782884f8739fb52a8119a16", size = 12155842 },
{ url = "https://files.pythonhosted.org/packages/89/9e/700ca289f172a38eb0bca752056d0a42637fa17b81649b9331786cb791d7/ruff-0.9.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de42e4edc296f520bb84954eb992a07a0ec5a02fecb834498415908469854a52", size = 11613804 },
{ url = "https://files.pythonhosted.org/packages/f2/92/648020b3b5db180f41a931a68b1c8575cca3e63cec86fd26807422a0dbad/ruff-0.9.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d257f95b65806104b6b1ffca0ea53f4ef98454036df65b1eda3693534813ecd1", size = 13823776 },
{ url = "https://files.pythonhosted.org/packages/5e/a6/cc472161cd04d30a09d5c90698696b70c169eeba2c41030344194242db45/ruff-0.9.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b60dec7201c0b10d6d11be00e8f2dbb6f40ef1828ee75ed739923799513db24c", size = 11302673 },
{ url = "https://files.pythonhosted.org/packages/6c/db/d31c361c4025b1b9102b4d032c70a69adb9ee6fde093f6c3bf29f831c85c/ruff-0.9.10-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:d838b60007da7a39c046fcdd317293d10b845001f38bcb55ba766c3875b01e43", size = 10235358 },
{ url = "https://files.pythonhosted.org/packages/d1/86/d6374e24a14d4d93ebe120f45edd82ad7dcf3ef999ffc92b197d81cdc2a5/ruff-0.9.10-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:ccaf903108b899beb8e09a63ffae5869057ab649c1e9231c05ae354ebc62066c", size = 9886177 },
{ url = "https://files.pythonhosted.org/packages/00/62/a61691f6eaaac1e945a1f3f59f1eea9a218513139d5b6c2b8f88b43b5b8f/ruff-0.9.10-py3-none-musllinux_1_2_i686.whl", hash = "sha256:f9567d135265d46e59d62dc60c0bfad10e9a6822e231f5b24032dba5a55be6b5", size = 10864747 },
{ url = "https://files.pythonhosted.org/packages/ee/94/2c7065e1d92a8a8a46d46d9c3cf07b0aa7e0a1e0153d74baa5e6620b4102/ruff-0.9.10-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5f202f0d93738c28a89f8ed9eaba01b7be339e5d8d642c994347eaa81c6d75b8", size = 11360441 },
{ url = "https://files.pythonhosted.org/packages/a7/8f/1f545ea6f9fcd7bf4368551fb91d2064d8f0577b3079bb3f0ae5779fb773/ruff-0.9.10-py3-none-win32.whl", hash = "sha256:bfb834e87c916521ce46b1788fbb8484966e5113c02df216680102e9eb960029", size = 10247401 },
{ url = "https://files.pythonhosted.org/packages/4f/18/fb703603ab108e5c165f52f5b86ee2aa9be43bb781703ec87c66a5f5d604/ruff-0.9.10-py3-none-win_amd64.whl", hash = "sha256:f2160eeef3031bf4b17df74e307d4c5fb689a6f3a26a2de3f7ef4044e3c484f1", size = 11366360 },
{ url = "https://files.pythonhosted.org/packages/35/85/338e603dc68e7d9994d5d84f24adbf69bae760ba5efd3e20f5ff2cec18da/ruff-0.9.10-py3-none-win_arm64.whl", hash = "sha256:5fd804c0327a5e5ea26615550e706942f348b197d5475ff34c19733aee4b2e69", size = 10436892 },
{ url = "https://files.pythonhosted.org/packages/73/b2/af7c2cc9e438cbc19fafeec4f20bfcd72165460fe75b2b6e9a0958c8c62b/ruff-0.9.10-py3-none-linux_armv6l.whl", hash = "sha256:eb4d25532cfd9fe461acc83498361ec2e2252795b4f40b17e80692814329e42d", size = 10049494, upload-time = "2025-03-07T15:26:51.268Z" },
{ url = "https://files.pythonhosted.org/packages/6d/12/03f6dfa1b95ddd47e6969f0225d60d9d7437c91938a310835feb27927ca0/ruff-0.9.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:188a6638dab1aa9bb6228a7302387b2c9954e455fb25d6b4470cb0641d16759d", size = 10853584, upload-time = "2025-03-07T15:26:56.104Z" },
{ url = "https://files.pythonhosted.org/packages/02/49/1c79e0906b6ff551fb0894168763f705bf980864739572b2815ecd3c9df0/ruff-0.9.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:5284dcac6b9dbc2fcb71fdfc26a217b2ca4ede6ccd57476f52a587451ebe450d", size = 10155692, upload-time = "2025-03-07T15:27:01.385Z" },
{ url = "https://files.pythonhosted.org/packages/5b/01/85e8082e41585e0e1ceb11e41c054e9e36fed45f4b210991052d8a75089f/ruff-0.9.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:47678f39fa2a3da62724851107f438c8229a3470f533894b5568a39b40029c0c", size = 10369760, upload-time = "2025-03-07T15:27:04.023Z" },
{ url = "https://files.pythonhosted.org/packages/a1/90/0bc60bd4e5db051f12445046d0c85cc2c617095c0904f1aa81067dc64aea/ruff-0.9.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:99713a6e2766b7a17147b309e8c915b32b07a25c9efd12ada79f217c9c778b3e", size = 9912196, upload-time = "2025-03-07T15:27:06.93Z" },
{ url = "https://files.pythonhosted.org/packages/66/ea/0b7e8c42b1ec608033c4d5a02939c82097ddcb0b3e393e4238584b7054ab/ruff-0.9.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:524ee184d92f7c7304aa568e2db20f50c32d1d0caa235d8ddf10497566ea1a12", size = 11434985, upload-time = "2025-03-07T15:27:10.082Z" },
{ url = "https://files.pythonhosted.org/packages/d5/86/3171d1eff893db4f91755175a6e1163c5887be1f1e2f4f6c0c59527c2bfd/ruff-0.9.10-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:df92aeac30af821f9acf819fc01b4afc3dfb829d2782884f8739fb52a8119a16", size = 12155842, upload-time = "2025-03-07T15:27:12.727Z" },
{ url = "https://files.pythonhosted.org/packages/89/9e/700ca289f172a38eb0bca752056d0a42637fa17b81649b9331786cb791d7/ruff-0.9.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de42e4edc296f520bb84954eb992a07a0ec5a02fecb834498415908469854a52", size = 11613804, upload-time = "2025-03-07T15:27:15.944Z" },
{ url = "https://files.pythonhosted.org/packages/f2/92/648020b3b5db180f41a931a68b1c8575cca3e63cec86fd26807422a0dbad/ruff-0.9.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d257f95b65806104b6b1ffca0ea53f4ef98454036df65b1eda3693534813ecd1", size = 13823776, upload-time = "2025-03-07T15:27:18.996Z" },
{ url = "https://files.pythonhosted.org/packages/5e/a6/cc472161cd04d30a09d5c90698696b70c169eeba2c41030344194242db45/ruff-0.9.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b60dec7201c0b10d6d11be00e8f2dbb6f40ef1828ee75ed739923799513db24c", size = 11302673, upload-time = "2025-03-07T15:27:21.655Z" },
{ url = "https://files.pythonhosted.org/packages/6c/db/d31c361c4025b1b9102b4d032c70a69adb9ee6fde093f6c3bf29f831c85c/ruff-0.9.10-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:d838b60007da7a39c046fcdd317293d10b845001f38bcb55ba766c3875b01e43", size = 10235358, upload-time = "2025-03-07T15:27:24.72Z" },
{ url = "https://files.pythonhosted.org/packages/d1/86/d6374e24a14d4d93ebe120f45edd82ad7dcf3ef999ffc92b197d81cdc2a5/ruff-0.9.10-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:ccaf903108b899beb8e09a63ffae5869057ab649c1e9231c05ae354ebc62066c", size = 9886177, upload-time = "2025-03-07T15:27:27.282Z" },
{ url = "https://files.pythonhosted.org/packages/00/62/a61691f6eaaac1e945a1f3f59f1eea9a218513139d5b6c2b8f88b43b5b8f/ruff-0.9.10-py3-none-musllinux_1_2_i686.whl", hash = "sha256:f9567d135265d46e59d62dc60c0bfad10e9a6822e231f5b24032dba5a55be6b5", size = 10864747, upload-time = "2025-03-07T15:27:30.637Z" },
{ url = "https://files.pythonhosted.org/packages/ee/94/2c7065e1d92a8a8a46d46d9c3cf07b0aa7e0a1e0153d74baa5e6620b4102/ruff-0.9.10-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5f202f0d93738c28a89f8ed9eaba01b7be339e5d8d642c994347eaa81c6d75b8", size = 11360441, upload-time = "2025-03-07T15:27:33.356Z" },
{ url = "https://files.pythonhosted.org/packages/a7/8f/1f545ea6f9fcd7bf4368551fb91d2064d8f0577b3079bb3f0ae5779fb773/ruff-0.9.10-py3-none-win32.whl", hash = "sha256:bfb834e87c916521ce46b1788fbb8484966e5113c02df216680102e9eb960029", size = 10247401, upload-time = "2025-03-07T15:27:35.994Z" },
{ url = "https://files.pythonhosted.org/packages/4f/18/fb703603ab108e5c165f52f5b86ee2aa9be43bb781703ec87c66a5f5d604/ruff-0.9.10-py3-none-win_amd64.whl", hash = "sha256:f2160eeef3031bf4b17df74e307d4c5fb689a6f3a26a2de3f7ef4044e3c484f1", size = 11366360, upload-time = "2025-03-07T15:27:38.66Z" },
{ url = "https://files.pythonhosted.org/packages/35/85/338e603dc68e7d9994d5d84f24adbf69bae760ba5efd3e20f5ff2cec18da/ruff-0.9.10-py3-none-win_arm64.whl", hash = "sha256:5fd804c0327a5e5ea26615550e706942f348b197d5475ff34c19733aee4b2e69", size = 10436892, upload-time = "2025-03-07T15:27:41.687Z" },
]
[[package]]
name = "shellingham"
version = "1.5.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310 }
sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755 },
{ url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" },
]
[[package]]
@@ -407,25 +409,25 @@ dependencies = [
{ name = "shellingham" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/8b/6f/3991f0f1c7fcb2df31aef28e0594d8d54b05393a0e4e34c65e475c2a5d41/typer-0.15.2.tar.gz", hash = "sha256:ab2fab47533a813c49fe1f16b1a370fd5819099c00b119e0633df65f22144ba5", size = 100711 }
sdist = { url = "https://files.pythonhosted.org/packages/8b/6f/3991f0f1c7fcb2df31aef28e0594d8d54b05393a0e4e34c65e475c2a5d41/typer-0.15.2.tar.gz", hash = "sha256:ab2fab47533a813c49fe1f16b1a370fd5819099c00b119e0633df65f22144ba5", size = 100711, upload-time = "2025-02-27T19:17:34.807Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7f/fc/5b29fea8cee020515ca82cc68e3b8e1e34bb19a3535ad854cac9257b414c/typer-0.15.2-py3-none-any.whl", hash = "sha256:46a499c6107d645a9c13f7ee46c5d5096cae6f5fc57dd11eccbbb9ae3e44ddfc", size = 45061 },
{ url = "https://files.pythonhosted.org/packages/7f/fc/5b29fea8cee020515ca82cc68e3b8e1e34bb19a3535ad854cac9257b414c/typer-0.15.2-py3-none-any.whl", hash = "sha256:46a499c6107d645a9c13f7ee46c5d5096cae6f5fc57dd11eccbbb9ae3e44ddfc", size = 45061, upload-time = "2025-02-27T19:17:32.111Z" },
]
[[package]]
name = "typing-extensions"
version = "4.12.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 }
sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321, upload-time = "2024-06-07T18:52:15.995Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 },
{ url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438, upload-time = "2024-06-07T18:52:13.582Z" },
]
[[package]]
name = "urllib3"
version = "2.3.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/aa/63/e53da845320b757bf29ef6a9062f5c669fe997973f966045cb019c3f4b66/urllib3-2.3.0.tar.gz", hash = "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d", size = 307268 }
sdist = { url = "https://files.pythonhosted.org/packages/aa/63/e53da845320b757bf29ef6a9062f5c669fe997973f966045cb019c3f4b66/urllib3-2.3.0.tar.gz", hash = "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d", size = 307268, upload-time = "2024-12-22T07:47:30.032Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c8/19/4ec628951a74043532ca2cf5d97b7b14863931476d117c471e8e2b1eb39f/urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df", size = 128369 },
{ url = "https://files.pythonhosted.org/packages/c8/19/4ec628951a74043532ca2cf5d97b7b14863931476d117c471e8e2b1eb39f/urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df", size = 128369, upload-time = "2024-12-22T07:47:28.074Z" },
]