diff --git a/.github/workflows/build-python-wheels.yml b/.github/workflows/build-python-wheels.yml new file mode 100644 index 000000000..e69f3eec1 --- /dev/null +++ b/.github/workflows/build-python-wheels.yml @@ -0,0 +1,122 @@ +--- +name: Build Python Wheels + +on: + pull_request: + push: + branches: + - "main" + - "rc" + - "hotfix-rc" + workflow_dispatch: + +defaults: + run: + shell: bash + working-directory: languages/python + +jobs: + generate_schemas: + uses: ./.github/workflows/generate_schemas.yml + + setup: + name: Setup + runs-on: ubuntu-22.04 + outputs: + package_version: ${{ steps.retrieve-version.outputs.package_version }} + steps: + - name: Checkout repo + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + + - name: Get Package Version + id: retrieve-version + run: | + VERSION="$(grep -o '^version = ".*"' ../../crates/bitwarden-py/Cargo.toml | grep -Eo "[0-9]+\.[0-9]+\.[0-9]+")" + echo "package_version=$VERSION" >> $GITHUB_OUTPUT + + build: + name: Building Python wheel for - ${{ matrix.settings.os }} - ${{ matrix.settings.target }} + runs-on: ${{ matrix.settings.os || 'ubuntu-latest' }} + needs: + - generate_schemas + - setup + env: + _PACKAGE_VERSION: ${{ needs.setup.outputs.package_version }} + strategy: + fail-fast: false + matrix: + settings: + - os: macos-12 + target: x86_64-apple-darwin + + - os: macos-12 + target: aarch64-apple-darwin + + - os: windows-2022 + target: x86_64-pc-windows-msvc + + - os: ubuntu-22.04 + target: x86_64-unknown-linux-gnu + + - os: ubuntu-22.04 + target: aarch64-unknown-linux-gnu + + steps: + - name: Checkout repo + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + + - name: Setup Node + uses: actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65 # v4.0.0 + with: + node-version: 18 + + - name: Install rust + uses: dtolnay/rust-toolchain@439cf607258077187679211f12aa6f19af4a0af7 # stable + with: + toolchain: stable + targets: ${{ matrix.settings.target }} + + - name: Cache cargo registry + uses: Swatinem/rust-cache@3cf7f8cc28d1b4e7d01e3783be10a97d55d483c8 # v2.7.1 + with: + key: ${{ matrix.settings.target }}-cargo-${{ matrix.settings.os }} + + - name: Retrieve schemas + uses: actions/download-artifact@f44cd7b40bfd40b6aa1cc1b9b5b7bf03d3c67110 # v4.1.0 + with: + name: schemas.py + path: ${{ github.workspace }}/languages/python/bitwarden_sdk + + - name: Build wheels + if: ${{ matrix.settings.target != 'x86_64-unknown-linux-gnu' }} + uses: PyO3/maturin-action@b9e8f88fd4448fdecf5095864cdc7e39a544aa9f # v1.40.7 + with: + target: ${{ matrix.settings.target }} + args: --release --find-interpreter --sdist + sccache: "true" + manylinux: "2_28" # https://github.com/pola-rs/polars/pull/12211 + working-directory: ${{ github.workspace }}/languages/python + + - name: Build wheels (Linux - x86_64) + if: ${{ matrix.settings.target == 'x86_64-unknown-linux-gnu' }} + uses: PyO3/maturin-action@b9e8f88fd4448fdecf5095864cdc7e39a544aa9f # v1.40.7 + with: + target: ${{ matrix.settings.target }} + args: --release --find-interpreter --sdist + container: quay.io/pypa/manylinux_2_28_x86_64:2023-11-20-745eb52 + sccache: "true" + manylinux: "2_28" # https://github.com/pola-rs/polars/pull/12211 + working-directory: ${{ github.workspace }}/languages/python + + - name: Upload wheels + uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3 + with: + name: bitwarden_sdk-${{ env._PACKAGE_VERSION }}-${{ matrix.settings.target }} + path: ${{ github.workspace }}/target/wheels/bitwarden_sdk*.whl + + - name: Upload sdists + if: ${{ matrix.settings.target == 'x86_64-unknown-linux-gnu' }} # we only need one sdist + uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3 + with: + name: bitwarden_sdk-${{ env._PACKAGE_VERSION }}-sdist + path: ${{ github.workspace }}/target/wheels/bitwarden_sdk-*.tar.gz diff --git a/languages/python/README.md b/languages/python/README.md index 871be274f..e77e7a8eb 100644 --- a/languages/python/README.md +++ b/languages/python/README.md @@ -1,31 +1,32 @@ -# Requirements - -- Python3 -- setuptools - ```bash - pip install setuptools - ``` -- setuptools_rust - ```bash - pip install setuptools_rust - ``` -- dateutil - ```bash - pip install python-dateutil - ``` - -# Installation - -From the `languages/python/` directory, +# Build locally +## Requirements +- Python 3 +- `maturin` (install with `pip install maturin[patchelf]`) +- `npm` + +## Build + +From the root of the repository: ```bash -python3 ./setup.py develop +npm run schemas # generate schemas.py + +cd languages/python/ +maturin develop ``` -Rename the the resulting `.so` file to `bitwarden_py.so`, if it isn't already there. +You can now import `BitwardenClient` in your Python code. + +# Use without building locally + +```bash +pip install BitwardenClient +``` # Run +Set the `ORGANIZATION_ID` and `ACCESS_TOKEN` environment variables to your organization ID and access token, respectively. + ```bash -python3 ./login.py +python3 ./example.py ``` diff --git a/languages/python/bitwarden_sdk/__init__.py b/languages/python/bitwarden_sdk/__init__.py index e69de29bb..b2aeffea1 100644 --- a/languages/python/bitwarden_sdk/__init__.py +++ b/languages/python/bitwarden_sdk/__init__.py @@ -0,0 +1,13 @@ +"""The official Bitwarden client library for Python.""" + +__version__ = "0.1.0" + +from .bitwarden_client import * +from .schemas import * + +__doc__ = bitwarden_client.__doc__ +if hasattr(bitwarden_client, "__all__"): + __all__ = bitwarden_client.__all__ + +if hasattr(schemas, "__all__"): + __all__ += schemas.__all__ diff --git a/languages/python/example.py b/languages/python/example.py old mode 100644 new mode 100755 index b3f2ab006..16367a0c5 --- a/languages/python/example.py +++ b/languages/python/example.py @@ -1,29 +1,35 @@ -import json +#!/usr/bin/env python3 import logging -import sys -from bitwarden_sdk.bitwarden_client import BitwardenClient -from bitwarden_sdk.schemas import client_settings_from_dict, DeviceType +import os + +from bitwarden_sdk import BitwardenClient, DeviceType, client_settings_from_dict # Create the BitwardenClient, which is used to interact with the SDK -client = BitwardenClient(client_settings_from_dict({ - "apiUrl": "http://localhost:4000", - "deviceType": DeviceType.SDK, - "identityUrl": "http://localhost:33656", - "userAgent": "Python", -})) +client = BitwardenClient( + client_settings_from_dict( + { + "apiUrl": os.getenv("API_URL", "http://localhost:4000"), + "deviceType": DeviceType.SDK, + "identityUrl": os.getenv("IDENTITY_URL", "http://localhost:33656"), + "userAgent": "Python", + } + ) +) # Add some logging & set the org id logging.basicConfig(level=logging.DEBUG) -organization_id = "org_id_here" +organization_id = os.getenv("ORGANIZATION_ID") # Attempt to authenticate with the Secrets Manager Access Token -client.access_token_login("access_token_here") +client.access_token_login(os.getenv("ACCESS_TOKEN")) # -- Example Project Commands -- project = client.projects().create("ProjectName", organization_id) project2 = client.projects().create("Project - Don't Delete Me!", organization_id) -updated_project = client.projects().update(project.data.id, "Cool New Project Name", organization_id) +updated_project = client.projects().update( + project.data.id, "Cool New Project Name", organization_id +) get_that_project = client.projects().get(project.data.id) input("Press Enter to delete the project...") @@ -33,9 +39,28 @@ # -- Example Secret Commands -- -secret = client.secrets().create("TEST_SECRET", "This is a test secret", organization_id, "Secret1234!", [project2.data.id]) -secret2 = client.secrets().create("Secret - Don't Delete Me!", "This is a test secret that will stay", organization_id, "Secret1234!", [project2.data.id]) -secret_updated = client.secrets().update(secret.data.id, "TEST_SECRET_UPDATED", "This as an updated test secret", organization_id, "Secret1234!_updated", [project2.data.id]) +secret = client.secrets().create( + "TEST_SECRET", + "This is a test secret", + organization_id, + "Secret1234!", + [project2.data.id], +) +secret2 = client.secrets().create( + "Secret - Don't Delete Me!", + "This is a test secret that will stay", + organization_id, + "Secret1234!", + [project2.data.id], +) +secret_updated = client.secrets().update( + secret.data.id, + "TEST_SECRET_UPDATED", + "This as an updated test secret", + organization_id, + "Secret1234!_updated", + [project2.data.id], +) secret_retrieved = client.secrets().get(secret.data.id) input("Press Enter to delete the secret...") diff --git a/languages/python/pyproject.toml b/languages/python/pyproject.toml new file mode 100644 index 000000000..28bb22507 --- /dev/null +++ b/languages/python/pyproject.toml @@ -0,0 +1,29 @@ +[build-system] +build-backend = "maturin" +requires = ["maturin>=1.0,<2.0", "setuptools_rust>=1.8.1"] + +[project] +authors = [{ name = "Bitwarden", email = "support@bitwarden.com" }] +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "License :: Other/Proprietary License", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Rust", + "Topic :: Security", +] +dependencies = ["dateutils >= 0.6.6"] +description = "A Bitwarden Client for python" +name = "bitwarden_sdk" +readme = "README.md" +requires-python = ">=3.0" +version = "0.1.0" + +[tool.maturin] +bindings = "pyo3" +compatibility = "2_28" +include = [ + { path = "bitwarden_sdk/*.py", format = ["sdist", "wheel"] } +] +manifest-path = "../../crates/bitwarden-py/Cargo.toml" +python-packages = ["bitwarden_sdk"] diff --git a/languages/python/setup.py b/languages/python/setup.py deleted file mode 100644 index b243a4fe8..000000000 --- a/languages/python/setup.py +++ /dev/null @@ -1,12 +0,0 @@ -from setuptools import setup -from setuptools_rust import Binding, RustExtension - -setup( - name="bitwarden_sdk", - description="A Bitwarden Client for python", - version="0.1", - rust_extensions=[RustExtension( - "bitwarden_py", path="../../crates/bitwarden-py/Cargo.toml", binding=Binding.PyO3)], - packages=['bitwarden_sdk'], - zip_safe=False, -)