diff --git a/.github/workflows/templates_python.yml b/.github/workflows/templates_python.yml index db54be81d..180b79db9 100644 --- a/.github/workflows/templates_python.yml +++ b/.github/workflows/templates_python.yml @@ -8,6 +8,9 @@ on: - main workflow_dispatch: +env: + MAIN_PYTHON_VERSION: '3.8' + concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true @@ -15,7 +18,7 @@ concurrency: jobs: tests: - name: ${{ matrix.cfg.template }} / ${{ matrix.cfg.build-system }} / ${{ matrix.python-version }} + name: "${{ matrix.cfg.template }} / ${{ matrix.cfg.build-system }} / ${{ matrix.python-version }}" runs-on: ubuntu-latest strategy: matrix: @@ -29,7 +32,6 @@ jobs: - python-version: "3.11" toxenv: py311 toxextra: keep-output - cfg: # All pybasic template tests - {name: "doc-project", template: "doc-project", build-system: "flit", outdir: "doc_proje0/doc-project"} @@ -47,6 +49,9 @@ jobs: - {name: "pyansys-advanced-poetry", template: "pyansys-advanced", build-system: "poetry", outdir: "pyansys_a1/pyproduct-library"} - {name: "pyansys-advanced-setuptools", template: "pyansys-advanced", build-system: "setuptools", outdir: "pyansys_a2/pyproduct-library"} + # All ansys API templates + - {name: "ansys-api", template: "ansys-api", build-system: "setuptools", outdir: "ansys_api2/ansys-api-product-library"} + # # All pyace template tests - {name: "pyace-pkg", template: "pyace", build-system: "setuptools", outdir: "pyace_set0/project"} - {name: "pyace-flask", template: "pyace-flask", build-system: "setuptools", outdir: "pyace_fla2/project"} @@ -57,25 +62,26 @@ jobs: - {name: "solution", template: "solution", build-system: "poetry", outdir: "solution_1/solution"} fail-fast: false - steps: - - uses: actions/checkout@v3 - - name: Set up Python ${{ matrix.python-version }} + - name: "Checkout project" + uses: actions/checkout@v3 + + - name: "Set up Python ${{ matrix.python-version }}" uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - - name: Install dependencies + - name: "Install dependencies" run: | python -m pip install --upgrade pip tox - - name: Bake ${{ matrix.cfg.template }} + - name: "Bake ${{ matrix.cfg.template }}" run: | export TEMPLATE=${{ matrix.cfg.template }} - tox -e ${{ matrix.toxenv }}-${{ matrix.toxextra }}-template + tox -r -e ${{ matrix.toxenv }}-${{ matrix.toxextra }}-template - - name: Move baked project to repo again - if: matrix.python-version == '3.8' && github.event_name == 'push' + - name: "Move baked project to repo again" + if: matrix.python-version == env.MAIN_PYTHON_VERSION && github.event_name == 'push' run: | mv "output/test_template_python_${{ matrix.cfg.outdir }}" baked_template # GitHub Apps are not allowed to deal with .github workflows @@ -83,7 +89,7 @@ jobs: ls -a baked_template - name: Create demo branch - if: matrix.python-version == '3.8' && github.event_name == 'push' + if: matrix.python-version == env.MAIN_PYTHON_VERSION && github.event_name == 'push' uses: peterjgrainger/action-create-branch@v2.4.0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -91,7 +97,7 @@ jobs: branch: "demo/${{ matrix.cfg.name }}" - name: Publish demo branch - if: matrix.python-version == '3.8' && github.event_name == 'push' + if: matrix.python-version == env.MAIN_PYTHON_VERSION && github.event_name == 'push' uses: s0/git-publish-subdir-action@develop env: REPO: self diff --git a/src/ansys/templates/__init__.py b/src/ansys/templates/__init__.py index ec936e1c5..b778f36da 100644 --- a/src/ansys/templates/__init__.py +++ b/src/ansys/templates/__init__.py @@ -39,6 +39,7 @@ "pybasic": "Create a basic Python Package.", "pyansys": "Create a PyAnsys Python Package project.", "pyansys-advanced": "Create an advanced PyAnsys Python Package project.", + "ansys-api": "Create a new gRPC API definition.", "pyansys-openapi_client": "Create an OpenAPI Client Package project.", "pyace": "Create a Python project for any method developers.", "pyace-flask": "Create a Flask project initialized for any developer.", diff --git a/src/ansys/templates/cli.py b/src/ansys/templates/cli.py index 330c053c8..77269341e 100644 --- a/src/ansys/templates/cli.py +++ b/src/ansys/templates/cli.py @@ -93,6 +93,12 @@ def pyansys_advanced(): create_project("pyansys-advanced") +@new.command() +def ansys_api(): + """Create a new gRPC API definition.""" + create_project("ansys-api") + + @new.command() def pyansys_openapi_client(): """Create an OpenAPI Client Package project.""" diff --git a/src/ansys/templates/paths.py b/src/ansys/templates/paths.py index 6e5bcc267..828513f71 100644 --- a/src/ansys/templates/paths.py +++ b/src/ansys/templates/paths.py @@ -48,6 +48,9 @@ PYTHON_TEMPLATES_PYANSYS_ADVANCED_PATH = PYTHON_TEMPLATES_PATH / "pyansys_advanced" """Path to the advanced PyAnsys Python Package template.""" +PYTHON_TEMPLATES_ANSYS_API_PATH = PYTHON_TEMPLATES_PATH / "ansys_api" +"""Path to the Ansys API Python Package template.""" + PYTHON_TEMPLATES_PYANSYS_OPENAPI_CLIENT_PATH = PYTHON_TEMPLATES_PATH / "pyansys_openapi_client" """Path to the PyAnsys OpenAPI Client Package template.""" @@ -79,6 +82,7 @@ "pybasic": PYTHON_TEMPLATES_PYBASIC_PATH, "pyansys": PYTHON_TEMPLATES_PYANSYS_PATH, "pyansys-advanced": PYTHON_TEMPLATES_PYANSYS_ADVANCED_PATH, + "ansys-api": PYTHON_TEMPLATES_ANSYS_API_PATH, "pyansys-openapi-client": PYTHON_TEMPLATES_PYANSYS_OPENAPI_CLIENT_PATH, "pyace": PYTHON_TEMPLATES_PYACE_PATH, "pyace-grpc": PYTHON_TEMPLATES_PYACE_GRPC_PATH, diff --git a/src/ansys/templates/python/ansys_api/cookiecutter.json b/src/ansys/templates/python/ansys_api/cookiecutter.json new file mode 100644 index 000000000..132df0cf2 --- /dev/null +++ b/src/ansys/templates/python/ansys_api/cookiecutter.json @@ -0,0 +1,30 @@ +{ + "__template_name": "ansys-api", + "product_name": "", + "__product_name_slug": "{{ cookiecutter.product_name | slugify(separator=('_')) }}", + "library_name": "", + "__library_name_slug": "{{ cookiecutter.library_name | slugify(separator=('_')) }}", + "__project_name_slug": "ansys-api-{{ cookiecutter.__product_name_slug }}{% if cookiecutter.__library_name_slug %}-{{ cookiecutter.__library_name_slug }}{% endif %}", + "__pkg_name": "{{ cookiecutter.__project_name_slug }}", + "api_version": "0", + "__api_version": "{{ cookiecutter.api_version }}", + "__pkg_namespace": "ansys.api.{{ cookiecutter.__product_name_slug }}.{{ cookiecutter.__library_name_slug }}", + "__pkg_namespace_with_version": "{{ cookiecutter.__pkg_namespace}}-v{{ cookiecutter.__api_version }}", + "version": "0.1.dev0", + "__version": "{{ cookiecutter.version }}", + "short_description": "A collection of gRPC API definitions for Ansys {{ cookiecutter.product_name }} {{ cookiecutter.library_name }}", + "__short_description": "{{ cookiecutter.short_description | capitalize }}", + "protos_dir": "", + "proto_dependencies": { + "modules": [] + }, + "__requires_python": "", + "__repository_url": "", + "__build_system": "setuptools", + "__logo": "", + "__logo_color": "", + "__max_linelength": "", + "__coverage_source": "", + "__is_pyansys": "false", + "_extensions": ["cookiecutter.extensions.SlugifyExtension"] +} diff --git a/src/ansys/templates/python/ansys_api/hooks/post_gen_project.py b/src/ansys/templates/python/ansys_api/hooks/post_gen_project.py new file mode 100644 index 000000000..c0b0c6f76 --- /dev/null +++ b/src/ansys/templates/python/ansys_api/hooks/post_gen_project.py @@ -0,0 +1,27 @@ +"""Hook to copy the .proto files into the project, and create __init__.py and py.typed files.""" + +from ansys.templates.utils import keep_files + +src_path = "src/ansys/api/{{ cookiecutter.__product_name_slug }}" + +if "{{ cookiecutter.__library_name_slug }}" != "": + src_path += "/{{ cookiecutter.__library_name_slug }}" + +DESIRED_STRUCTURE = [ + "README.md", + "pyproject.toml", + "setup.py", + f"{src_path}/__init__.py", + f"{src_path}/VERSION", + f"{src_path}/py.typed", + ".github/workflows/ci.yml", +] +"""A list holding all desired files to be included in the project.""" + +def main(): + """Entry point of the script.""" + # Apply the desired structure to the project + keep_files(DESIRED_STRUCTURE) + +if __name__ == "__main__": + main() diff --git a/src/ansys/templates/python/ansys_api/{{ cookiecutter.__project_name_slug }}/.github/workflows/ci.yml b/src/ansys/templates/python/ansys_api/{{ cookiecutter.__project_name_slug }}/.github/workflows/ci.yml new file mode 100644 index 000000000..61682cfdf --- /dev/null +++ b/src/ansys/templates/python/ansys_api/{{ cookiecutter.__project_name_slug }}/.github/workflows/ci.yml @@ -0,0 +1,72 @@ +name: CI + +on: + pull_request: + push: + tags: + - "*" + branches: + - main + +env: + MAIN_PYTHON_VERSION: '3.10' + LIBRARY_NAME: '{{ cookiecutter.__pkg_namespace | replace(".", "-") }}' + LIBRARY_NAMESPACE: '{{ cookiecutter.__pkg_namespace }}' + +concurrency: + group: {{ '${{ github.workflow }}-${{ github.ref }}' }} + cancel-in-progress: true + +jobs: + + tests: + name: "Build package" + runs-on: ubuntu-latest + steps: + + - name: "Checkout repository" + uses: actions/checkout@v3 + + - name: "Setup Python" + uses: actions/setup-python@v4 + with: + python-version: {{ '${{ env.MAIN_PYTHON_VERSION }}' }} + + - name: "Install the library" + run: | + python -m pip install . + + - name: "Test import" + run: | + {{ 'python -c "from ${{ env.LIBRARY_NAMESPACE }} import __version__; print(__version__)"' }} + + build-library: + name: "Build library" + needs: tests + runs-on: ubuntu-latest + steps: + - uses: ansys/actions/build-library@v4 + with: + library-name: {{ '${{ env.LIBRARY_NAME }}' }} + + release: + name: "Release library" + if: github.event_name == 'push' && contains(github.ref, 'refs/tags') + needs: build-package + runs-on: ubuntu-latest + steps: + + - name: "Release to the private PyPI" + uses: ansys/actions/release-pypi-private@v4 + with: + library-name: {{ '${{ env.LIBRARY_NAME }}' }} + twine-username: "__token__" + twine-token: {{ '${{ secrets.PYANSYS_PYPI_PRIVATE_PAT }}' }} + + - name: "Release to GitHub" + uses: softprops/action-gh-release@v1 + with: + generate_release_notes: true + files: | + {{ '${{ env.LIBRARY_NAME }}-artifacts/*.whl }}' }} + {{ '${{ env.LIBRARY_NAME }}-artifacts/*.tar.gz' }} diff --git a/src/ansys/templates/python/ansys_api/{{ cookiecutter.__project_name_slug }}/README.md b/src/ansys/templates/python/ansys_api/{{ cookiecutter.__project_name_slug }}/README.md new file mode 100644 index 000000000..6653ddf33 --- /dev/null +++ b/src/ansys/templates/python/ansys_api/{{ cookiecutter.__project_name_slug }}/README.md @@ -0,0 +1,63 @@ +### {{ cookiecutter.__project_name_slug }} gRPC Interface Package + +This Python package contains the auto-generated gRPC Python interface files for +{{ cookiecutter.product_name }}. + + +#### Installation + +Provided that these wheels have been published to public PyPI, they can be +installed with: + +``` +pip install {{ cookiecutter.__project_name_slug | lower }} +``` + +Otherwise, see the + + +#### Build + +To build the gRPC packages, run: + +``` +pip install build +python -m build +``` + +This will create both the source distribution containing just the protofiles +along with the wheel containing the protofiles and build Python interface +files. + +Note that the interface files are identical regardless of the version of Python +used to generate them, but the last pre-built wheel for ``grpcio~=1.17`` was +Python 3.7, so to improve your build time, use Python 3.7 when building the +wheel. + + +#### Manual Deployment + +After building the packages, manually deploy them with: + +``` +pip install twine +twine upload dist/* +``` + +Note that this is automatically done through CI/CD. + + +#### Automatic Deployment + +This repository contains GitHub CI/CD that enables the automatic building of +source and wheel packages for these gRPC Python interface files. By default, +these are built on PRs, the main branch, and on tags when pushing. Artifacts +are uploaded for each PR. + +To publicly release wheels to PyPI, ensure your branch is up-to-date and then +push tags. For example, for the version ``v0.5.0``. + +```bash +git tag v0.5.0 +git push --tags +``` diff --git a/src/ansys/templates/python/ansys_api/{{ cookiecutter.__project_name_slug }}/pyproject.toml b/src/ansys/templates/python/ansys_api/{{ cookiecutter.__project_name_slug }}/pyproject.toml new file mode 100644 index 000000000..81deed0b5 --- /dev/null +++ b/src/ansys/templates/python/ansys_api/{{ cookiecutter.__project_name_slug }}/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["setuptools >= 42.0.0", "wheel", "ansys_tools_protoc_helper"{% for mod in cookiecutter.proto_dependencies['modules'] %}, "{{ mod }}"{% endfor %}] +build-backend = "setuptools.build_meta:__legacy__" diff --git a/src/ansys/templates/python/ansys_api/{{ cookiecutter.__project_name_slug }}/setup.py b/src/ansys/templates/python/ansys_api/{{ cookiecutter.__project_name_slug }}/setup.py new file mode 100644 index 000000000..182543428 --- /dev/null +++ b/src/ansys/templates/python/ansys_api/{{ cookiecutter.__project_name_slug }}/setup.py @@ -0,0 +1,51 @@ +"""Installation file for the {{ cookiecutter.__project_name_slug }} package""" + +import os +from datetime import datetime + +import setuptools + +from ansys.tools.protoc_helper import CMDCLASS_OVERRIDE + +# Get the long description from the README file +HERE = os.path.abspath(os.path.dirname(__file__)) +with open(os.path.join(HERE, "README.md"), encoding="utf-8") as f: + long_description = f.read() + +product = "{{ cookiecutter.__product_name_slug }}" +library = "{{ cookiecutter.__library_name_slug }}" +package_info = ["ansys", "api", product, library, "v{{ cookiecutter.__api_version }}"] +with open(os.path.join(HERE, "src", "ansys", "api", product, library, "VERSION"), encoding="utf-8") as f: + version = f.read().strip() + +package_name = "{{ cookiecutter.__project_name_slug }}" +dot_package_name = '.'.join(filter(None, package_info)) + +description = f"Autogenerated python gRPC interface package for {package_name}, built on {datetime.now().strftime('%H:%M:%S on %d %B %Y')}" + +if __name__ == "__main__": + setuptools.setup( + name=package_name, + version=version, + author="ANSYS, Inc.", + author_email='pyansys.core@ansys.com', + description=description, + short_description="{{ cookiecutter.__short_description }}", + long_description=long_description, + long_description_content_type='text/markdown', + url=f"https://github.com/ansys/{package_name}", + license="MIT", + python_requires=">=3.7", + install_requires=["grpcio~=1.17", "protobuf~=3.19"{% for mod in cookiecutter.proto_dependencies['modules'] %}, "{{ mod }}"{% endfor %}], + package_dir = {"": "src"}, + packages=setuptools.find_namespace_packages("src", include=("ansys.*",)), + package_data={ + "": ["*.proto", "*.pyi", "py.typed", "VERSION"], + }, + entry_points={ + "ansys.tools.protoc_helper.proto_provider": [ + f"{dot_package_name}={dot_package_name}" + ], + }, + cmdclass=CMDCLASS_OVERRIDE + ) diff --git a/src/ansys/templates/python/ansys_api/{{ cookiecutter.__project_name_slug }}/src/ansys/api/{{ cookiecutter.__product_name_slug }}/{{ cookiecutter.__library_name_slug }}/VERSION b/src/ansys/templates/python/ansys_api/{{ cookiecutter.__project_name_slug }}/src/ansys/api/{{ cookiecutter.__product_name_slug }}/{{ cookiecutter.__library_name_slug }}/VERSION new file mode 100644 index 000000000..e69de29bb diff --git a/src/ansys/templates/python/ansys_api/{{ cookiecutter.__project_name_slug }}/src/ansys/api/{{ cookiecutter.__product_name_slug }}/{{ cookiecutter.__library_name_slug }}/__init__.py b/src/ansys/templates/python/ansys_api/{{ cookiecutter.__project_name_slug }}/src/ansys/api/{{ cookiecutter.__product_name_slug }}/{{ cookiecutter.__library_name_slug }}/__init__.py new file mode 100644 index 000000000..5a4b1eb4b --- /dev/null +++ b/src/ansys/templates/python/ansys_api/{{ cookiecutter.__project_name_slug }}/src/ansys/api/{{ cookiecutter.__product_name_slug }}/{{ cookiecutter.__library_name_slug }}/__init__.py @@ -0,0 +1,8 @@ +"""Autogenerated Python gRPC interface package for {{ cookiecutter.__project_name_slug }}.""" + +import pathlib + +__all__ = ["__version__"] + +with open(pathlib.Path(__file__).parent / "VERSION", encoding="utf-8") as f: + __version__ = f.read().strip() diff --git a/src/ansys/templates/python/ansys_api/{{ cookiecutter.__project_name_slug }}/src/ansys/api/{{ cookiecutter.__product_name_slug }}/{{ cookiecutter.__library_name_slug }}/py.typed b/src/ansys/templates/python/ansys_api/{{ cookiecutter.__project_name_slug }}/src/ansys/api/{{ cookiecutter.__product_name_slug }}/{{ cookiecutter.__library_name_slug }}/py.typed new file mode 100644 index 000000000..e69de29bb diff --git a/tests/tests_templates/test_python_templates.py b/tests/tests_templates/test_python_templates.py index 8ab6dc7c4..e88d0cfe5 100644 --- a/tests/tests_templates/test_python_templates.py +++ b/tests/tests_templates/test_python_templates.py @@ -50,6 +50,14 @@ requires_python="3.7", ) +ANSYS_API_VARS = dict( + product_name="product", + library_name="library", + __product_name_slug="product", + __library_name_slug="library", + __project_name_slug="ansys-api-product-library", +) + PYANSYS_OPENAPI_VARS = dict( product_name="product", library_name="library", @@ -145,6 +153,17 @@ ["azure-pipeline.yml", "setup.py", ".coveragerc", "requirements_build.txt", "requirements_doc.txt", "requirements_tests.txt"]] +# Structure for pyansys-advanced projects +ANSYS_API_STRUCTURE = [ + "README.md", + "pyproject.toml", + "setup.py", + f"src/ansys/api/{ANSYS_API_VARS['__product_name_slug']}/{ANSYS_API_VARS['__library_name_slug']}/__init__.py", + f"src/ansys/api/{ANSYS_API_VARS['__product_name_slug']}/{ANSYS_API_VARS['__library_name_slug']}/py.typed", + f"src/ansys/api/{ANSYS_API_VARS['__product_name_slug']}/{ANSYS_API_VARS['__library_name_slug']}/VERSION", + ".github/workflows/ci.yml", +] + PYACE_FLASK_STRUCTURE = deepcopy(PYCOMMON_STRUCTURE) + [ "src/__init__.py", "src/_version.py", @@ -416,6 +435,7 @@ "pybasic": [PYBASIC_VARS, PYBASIC_STRUCTURE], "pyansys": [PYANSYS_VARS, PYANSYS_STRUCTURE], "pyansys-advanced": [PYANSYS_ADVANCED_VARS, PYANSYS_ADVANCED_STRUCTURE], + "ansys-api": [ANSYS_API_VARS, ANSYS_API_STRUCTURE], "pyansys-openapi-client": [PYANSYS_OPENAPI_VARS, PYANSYS_OPENAPI_STRUCTURE], "pyace-flask": [PYACE_VARS, PYACE_FLASK_STRUCTURE], "pyace-grpc": [PYACE_VARS, PYACE_GRPC_STRUCTURE], @@ -466,4 +486,6 @@ def test_template_python(tmp_path, build_system, template): keep_files(EXPECTED_STRUCTURE, project_path) # Check that all common files are included in baked project + print(EXPECTED_STRUCTURE) + print(project_path) assert_project_structure(EXPECTED_STRUCTURE, project_path)