diff --git a/.github/workflows/package.yml b/.github/workflows/package.yml new file mode 100644 index 0000000..da40b33 --- /dev/null +++ b/.github/workflows/package.yml @@ -0,0 +1,41 @@ +name: Validate distributions +on: + workflow_call: +permissions: + contents: read +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: '3.12' + - run: python -m pip install build twine + - run: python -m build + - run: python -m twine check --strict dist/* + - name: Check release tag matches package version + if: startsWith(github.ref, 'refs/tags/') + run: | + python - <<'PY' + import ast, os + from pathlib import Path + tree = ast.parse(Path('src/sysmlv2_client/__init__.py').read_text()) + version = next(ast.literal_eval(node.value) for node in tree.body + if isinstance(node, ast.Assign) + and any(isinstance(t, ast.Name) and t.id == '__version__' for t in node.targets)) + assert os.environ['GITHUB_REF_NAME'] == f'v{version}', 'Tag must match package version' + PY + - name: Test installed wheel outside checkout + run: | + python -m venv /tmp/wheel-test + /tmp/wheel-test/bin/python -m pip install dist/*.whl pytest requests-mock + cd /tmp + /tmp/wheel-test/bin/python -c "import sysml_api; from sysmlv2_client import SysMLV2Client" + /tmp/wheel-test/bin/python -m pytest --import-mode=importlib "$GITHUB_WORKSPACE/tests" + - name: Build wheel from source distribution + run: python -m pip wheel --no-deps dist/*.tar.gz --wheel-dir /tmp/sdist-wheels + - uses: actions/upload-artifact@v4 + with: + name: distributions + path: dist/* diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..6fa7509 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,38 @@ +name: Publish package +on: + release: + types: [published] + workflow_dispatch: +permissions: + contents: read +jobs: + build: + uses: ./.github/workflows/package.yml + testpypi: + if: github.event_name == 'workflow_dispatch' + needs: build + runs-on: ubuntu-latest + environment: testpypi + permissions: + id-token: write + steps: + - uses: actions/download-artifact@v4 + with: + name: distributions + path: dist/ + - uses: pypa/gh-action-pypi-publish@release/v1 + with: + repository-url: https://test.pypi.org/legacy/ + pypi: + if: github.event_name == 'release' && startsWith(github.ref, 'refs/tags/v') + needs: build + runs-on: ubuntu-latest + environment: pypi + permissions: + id-token: write + steps: + - uses: actions/download-artifact@v4 + with: + name: distributions + path: dist/ + - uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/.github/workflows/test-sysmlv2-python-client.yml b/.github/workflows/test-sysmlv2-python-client.yml index 2ab44a3..e6fd345 100644 --- a/.github/workflows/test-sysmlv2-python-client.yml +++ b/.github/workflows/test-sysmlv2-python-client.yml @@ -1,47 +1,28 @@ -name: Test sysmlv2-python-client - +name: Test sysml-api-client on: push: - branches: [ main, develop ] + branches: [main, develop] pull_request: - branches: [ main, develop ] - + branches: [main, develop] +permissions: + contents: read jobs: test: runs-on: ubuntu-latest - + strategy: + matrix: + python-version: ['3.10', '3.11', '3.12', '3.13', '3.14'] steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Set up Python - id: setup-python - uses: actions/setup-python@v4 - with: - python-version: '3.12' - - - name: Cache pip dependencies - uses: actions/cache@v3 - with: - path: ~/.cache/pip - key: ${{ runner.os }}-pip-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('pyproject.toml') }} - restore-keys: | - ${{ runner.os }}-pip- - - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install -e . - pip install pytest pytest-cov requests-mock - - - name: Run tests with coverage - run: | - mkdir -p test-results - pytest -v --tb=short --junitxml=test-results/results.xml --cov=src/sysmlv2_client --cov-report=xml:test-results/coverage.xml --cov-report=html:test-results/htmlcov - - - name: Upload test results - uses: actions/upload-artifact@v4 - if: always() - with: - name: test-results-sysmlv2-python-client - path: test-results/ + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + - run: python -m pip install -e ".[test]" + - run: python -m pytest -v --tb=short --junitxml=test-results/results.xml --cov=sysmlv2_client --cov-report=xml:test-results/coverage.xml + - uses: actions/upload-artifact@v4 + if: always() + with: + name: test-results-${{ matrix.python-version }} + path: test-results/ + package: + uses: ./.github/workflows/package.yml diff --git a/.gitignore b/.gitignore index c80c606..2afed4a 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ __pycache__/ # Virtual Environments venv/ .venv/ +.venv-wheel/ env/ # Jupyter Notebook Checkpoints @@ -23,7 +24,8 @@ env/ # Flexo Setup Sensitive Files flexo-setup/docker-compose/env/*.env /build -/src/sysmlv2_python_client.egg-info +/dist/ +*.egg-info/ /test-results coverage.xml /.venv-wheel diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..dec9c8f --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,6 @@ +# Changelog + +## 0.1.0 + +- Initial PyPI packaging as `sysml-api-client`. +- Includes the existing `sysmlv2_client` client, exceptions, and `sysml_api` helpers. diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..27854f7 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1 @@ +include CHANGELOG.md RELEASING.md diff --git a/README.md b/README.md index 41d8ab0..33f7166 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,20 @@ -# SysML v2 Python Client +# sysml-api-client -A basic Python client library for interacting with a SysML v2 API server, specifically tested against the OpenMBEE Flexo implementation. +A Python client for the [OMG Systems Modeling API and Services](https://www.omg.org/spec/SystemsModelingAPI), specifically tested against the OpenMBEE Flexo implementation. The PyPI name is `sysml-api-client`; Python imports remain `sysmlv2_client` and `sysml_api`. + +## Installation + +Requires Python 3.10 or newer. Once published on PyPI: + +```bash +python -m pip install sysml-api-client +``` + +```python +from sysmlv2_client import SysMLV2Client +``` + +See [RELEASING.md](RELEASING.md) for build validation and publisher setup. ## Features @@ -20,13 +34,8 @@ Follow instructions [here](https://github.com/Open-MBEE/flexo-mms-sysmlv2.git) ### 2. Install Client (Development) -```python -# Example: Add src to path if running scripts/notebooks from project root -import sys -import os -sys.path.insert(0, os.path.abspath('./src')) - -from sysmlv2_client import SysMLV2Client +```bash +python -m pip install -e ".[test]" ``` ## Basic Usage @@ -95,7 +104,6 @@ except Exception as e: ``` -See the [examples/basic_usage.ipynb](examples/basic_usage.ipynb) Jupyter Notebook for more detailed examples covering commits, branches, tags, and element retrieval/modification via commits. ## Running Tests @@ -103,7 +111,7 @@ Unit tests are implemented using `pytest` and `requests-mock`. 1. **Install Dependencies:** ```bash - pip install pytest requests requests-mock + python -m pip install -e ".[test]" ``` 2. **Run Tests:** Navigate to the project root directory in your terminal and run: ```bash diff --git a/RELEASING.md b/RELEASING.md new file mode 100644 index 0000000..f3acdad --- /dev/null +++ b/RELEASING.md @@ -0,0 +1,68 @@ +# Releasing sysml-api-client + +The distribution includes `sysmlv2_client` and `sysml_api`. The version comes +from `src/sysmlv2_client/__init__.py` (initial release: `0.1.0`). + +## One-time setup + +Register a pending Trusted Publisher from your PyPI organization's **Publishing** +page to create the project under organization ownership on the first upload. +If you manually create the project first, add a normal Trusted Publisher to +that project instead. + +| Publisher field | PyPI | TestPyPI | +| --- | --- | --- | +| Project name | `sysml-api-client` | `sysml-api-client` | +| GitHub owner | `Open-MBEE` | `Open-MBEE` | +| Repository | `sysmlv2-python-client` | `sysmlv2-python-client` | +| Workflow filename | `publish.yml` | `publish.yml` | +| Environment | `pypi` | `testpypi` | + +TestPyPI needs its own account and publisher configuration. Create GitHub +environments `pypi` and `testpypi`; configure required reviewers for `pypi` +and restrict it to release tags as appropriate. No PyPI token secret is needed. +Check that PyPI accepts the project name; a pending publisher does not reserve it. + +References: [organization pending publishers](https://blog.pypi.org/posts/2025-11-10-trusted-publishers-coming-to-orgs/), +[existing project publishers](https://docs.pypi.org/trusted-publishers/adding-a-publisher/). + +## Local validation + +Create and activate a virtual environment, then run: + +```bash +python -m pip install -e ".[test]" build twine +python -m pytest +python -m build +python -m twine check --strict dist/* +``` + +Start with an empty `dist/` directory. CI also tests the installed wheel outside +the checkout, imports both packages, and builds a wheel from the source archive. + +## Release + +1. Update `__version__` and `CHANGELOG.md`; each upload needs a new version. +2. Push the changes and wait for the test matrix and package checks to pass. +3. Run **Publish package** manually from the desired commit to upload to TestPyPI. + For repeated rehearsals use new versions, such as `0.1.0rc1`, `0.1.0rc2`. +4. In a clean virtual environment, verify the candidate (substitute its version): + + ```bash + python -m pip install requests + python -m pip install --index-url https://test.pypi.org/simple/ --no-deps sysml-api-client==0.1.0 + python -c "import sysml_api; from sysmlv2_client import SysMLV2Client" + ``` + +5. Publish a GitHub release tagged `v0.1.0` at the tested commit, substituting + the actual version. This triggers production publishing. Approve the `pypi` + environment deployment if configured. The workflow rejects mismatched tags. +6. Verify production installation in a fresh environment: + + ```bash + python -m pip install sysml-api-client==0.1.0 + python -c "import sysml_api; from sysmlv2_client import SysMLV2Client" + ``` + +Published files cannot be replaced on PyPI or TestPyPI. Increment the version +to correct a release. diff --git a/pyproject.toml b/pyproject.toml index 4b37c46..4a9b9b5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,10 +1,34 @@ [project] -name = "sysmlv2-python-client" -version = "0.1.0" -description = "Python client for SysML v2 services" -authors = [{name = "Your Name", email = "you@example.com"}] -dependencies = [] +name = "sysml-api-client" +dynamic = ["version"] +description = "Python client for the OMG Systems Modeling API and Services" +readme = "README.md" +requires-python = ">=3.10" +license = "Apache-2.0" +license-files = ["LICENSE"] +dependencies = ["requests>=2.28,<3"] +classifiers = [ + "Development Status :: 3 - Alpha", + "Programming Language :: Python :: 3", + "Operating System :: OS Independent", +] + +[project.urls] +Repository = "https://github.com/Open-MBEE/sysmlv2-python-client" +Issues = "https://github.com/Open-MBEE/sysmlv2-python-client/issues" +Specification = "https://www.omg.org/spec/SystemsModelingAPI" + +[project.optional-dependencies] +test = ["pytest>=7", "pytest-cov>=4", "requests-mock>=1.11"] + +[tool.setuptools.packages.find] +where = ["src"] +include = ["sysmlv2_client*", "sysml_api*"] +namespaces = false + +[tool.setuptools.dynamic] +version = {attr = "sysmlv2_client.__version__"} [build-system] -requires = ["setuptools>=61.0"] +requires = ["setuptools>=77.0.3"] build-backend = "setuptools.build_meta" diff --git a/src/sysml_api/api_lib.py b/src/sysml_api/api_lib.py index 2269efb..c3be767 100644 --- a/src/sysml_api/api_lib.py +++ b/src/sysml_api/api_lib.py @@ -1,4 +1,3 @@ -import numpy as np from sysmlv2_client import SysMLV2Client, SysMLV2Error, SysMLV2NotFoundError from typing import Any, Dict diff --git a/tests/test_client.py b/tests/test_client.py index b355b82..f7a4595 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -4,7 +4,7 @@ import requests_mock import requests -from src.sysmlv2_client import ( +from sysmlv2_client import ( SysMLV2Client, SysMLV2Error, SysMLV2AuthError,