pax_global_header00006660000000000000000000000064150413273260014515gustar00rootroot0000000000000052 comment=fc52ee27e2d70ce7c449cbb6685388da1fd82e40 trame-common-1.0.1/000077500000000000000000000000001504132732600141125ustar00rootroot00000000000000trame-common-1.0.1/.codespellrc000066400000000000000000000000401504132732600164040ustar00rootroot00000000000000[codespell] skip = CHANGELOG.md trame-common-1.0.1/.github/000077500000000000000000000000001504132732600154525ustar00rootroot00000000000000trame-common-1.0.1/.github/dependabot.yml000066400000000000000000000003401504132732600202770ustar00rootroot00000000000000version: 2 updates: # Maintain dependencies for GitHub Actions - package-ecosystem: "github-actions" directory: "/" schedule: interval: "weekly" groups: actions: patterns: - "*" trame-common-1.0.1/.github/release.yml000066400000000000000000000001141504132732600176110ustar00rootroot00000000000000changelog: exclude: authors: - dependabot - pre-commit-ci trame-common-1.0.1/.github/workflows/000077500000000000000000000000001504132732600175075ustar00rootroot00000000000000trame-common-1.0.1/.github/workflows/test_and_release.yml000066400000000000000000000051521504132732600235360ustar00rootroot00000000000000name: Test and Release on: workflow_dispatch: pull_request: push: branches: - main concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true env: # Many color libraries just need this to be set to any value, but at least # one distinguishes color depth, where "3" -> "256-bit color". FORCE_COLOR: 3 jobs: pre-commit: name: Format runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - uses: actions/setup-python@v5 with: python-version: "3.x" - uses: pre-commit/action@v3.0.1 with: extra_args: --hook-stage manual --all-files - name: Run Lint run: pipx run nox -s lint - name: Run Tests run: pipx run nox -s tests checks: name: Check Python ${{ matrix.python-version }} on ${{ matrix.runs-on }} runs-on: ${{ matrix.runs-on }} needs: [pre-commit] strategy: fail-fast: false matrix: python-version: ["3.9", "3.10", "3.13"] runs-on: [ubuntu-latest, windows-latest, macos-14] steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} allow-prereleases: true - name: Install package run: python -m pip install .[test] - name: Test package run: >- python -m pytest -ra --cov --cov-report=xml --cov-report=term --durations=20 - name: Upload coverage report uses: codecov/codecov-action@v5.4.3 with: token: ${{ secrets.CODECOV_TOKEN }} release: needs: [pre-commit, checks] name: Distribution build runs-on: ubuntu-latest if: github.event_name == 'push' permissions: id-token: write attestations: write contents: write environment: name: pypi url: https://pypi.org/p/trame-common steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - name: Python Semantic Release id: release uses: python-semantic-release/python-semantic-release@v10.2.0 with: github_token: ${{ secrets.GITHUB_TOKEN }} # for debug output # root_options: "-vv" - name: Generate artifact attestation for sdist and wheel if: steps.release.outputs.released == 'true' uses: actions/attest-build-provenance@v2.4.0 with: subject-path: "dist/*" - uses: pypa/gh-action-pypi-publish@release/v1 if: steps.release.outputs.released == 'true' trame-common-1.0.1/.gitignore000066400000000000000000000065631504132732600161140ustar00rootroot00000000000000# Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ share/python-wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .nox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover *.py,cover .hypothesis/ .pytest_cache/ cover/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py db.sqlite3 db.sqlite3-journal # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder .pybuilder/ target/ # Jupyter Notebook .ipynb_checkpoints # IPython profile_default/ ipython_config.py # pyenv # For a library or package, you might want to ignore these files since the code is # intended to run in multiple environments; otherwise, check them in: # .python-version # pipenv # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. # However, in case of collaboration, if having platform-specific dependencies or dependencies # having no cross-platform support, pipenv may install dependencies that don't work, or not # install all needed dependencies. #Pipfile.lock # UV # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. # This is especially recommended for binary packages to ensure reproducibility, and is more # commonly ignored for libraries. #uv.lock # poetry # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. # This is especially recommended for binary packages to ensure reproducibility, and is more # commonly ignored for libraries. # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control #poetry.lock # pdm # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. #pdm.lock # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it # in version control. # https://pdm.fming.dev/latest/usage/project/#working-with-version-control .pdm.toml .pdm-python .pdm-build/ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm __pypackages__/ # Celery stuff celerybeat-schedule celerybeat.pid # SageMath parsed files *.sage.py # Environments .env .venv env/ venv/ ENV/ env.bak/ venv.bak/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ .dmypy.json dmypy.json # Pyre type checker .pyre/ # pytype static type analyzer .pytype/ # Cython debug symbols cython_debug/ # PyCharm # JetBrains specific template is maintained in a separate JetBrains.gitignore that can # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. #.idea/ # Ruff stuff: .ruff_cache/ # PyPI configuration file .pypirc trame-common-1.0.1/.pre-commit-config.yaml000066400000000000000000000041611504132732600203750ustar00rootroot00000000000000ci: autoupdate_commit_msg: "chore: update pre-commit hooks" autofix_commit_msg: "style: pre-commit fixes" exclude: ^.cruft.json|.copier-answers.yml$ repos: - repo: https://github.com/adamchainz/blacken-docs rev: "1.19.1" hooks: - id: blacken-docs additional_dependencies: [black==24.*] - repo: https://github.com/pre-commit/pre-commit-hooks rev: "v5.0.0" hooks: - id: check-added-large-files - id: check-case-conflict - id: check-merge-conflict - id: check-symlinks - id: check-yaml - id: debug-statements - id: end-of-file-fixer - id: mixed-line-ending - id: name-tests-test args: ["--pytest-test-first"] - id: requirements-txt-fixer - id: trailing-whitespace - repo: https://github.com/pre-commit/pygrep-hooks rev: "v1.10.0" hooks: - id: rst-backticks - id: rst-directive-colons - id: rst-inline-touching-normal - repo: https://github.com/rbubley/mirrors-prettier rev: "v3.4.2" hooks: - id: prettier types_or: [yaml, markdown, html, css, scss, javascript, json] args: [--prose-wrap=always] - repo: https://github.com/astral-sh/ruff-pre-commit rev: "v0.9.1" hooks: - id: ruff args: ["--fix", "--show-fixes"] - id: ruff-format - repo: https://github.com/codespell-project/codespell rev: "v2.3.0" hooks: - id: codespell - repo: https://github.com/shellcheck-py/shellcheck-py rev: "v0.10.0.1" hooks: - id: shellcheck - repo: local hooks: - id: disallow-caps name: Disallow improper capitalization language: pygrep entry: PyBind|Numpy|Cmake|CCache|Github|PyTest exclude: .pre-commit-config.yaml - repo: https://github.com/abravalheri/validate-pyproject rev: "v0.23" hooks: - id: validate-pyproject additional_dependencies: ["validate-pyproject-schema-store[all]"] - repo: https://github.com/python-jsonschema/check-jsonschema rev: "0.31.0" hooks: - id: check-dependabot - id: check-github-workflows - id: check-readthedocs trame-common-1.0.1/.prettierignore000066400000000000000000000000151504132732600171510ustar00rootroot00000000000000CHANGELOG.md trame-common-1.0.1/CHANGELOG.md000066400000000000000000000005741504132732600157310ustar00rootroot00000000000000# CHANGELOG ## v0.2.0 (2025-03-31) ### Features - **version**: Add version utility function ([`85de2b2`](https://github.com/Kitware/trame-common/commit/85de2b2aaab1e10e26fff1f88efbd8f98eaeb1a8)) ## v0.1.0 (2025-03-31) ### Features - Gather common classes for trame ([`87bcd3f`](https://github.com/Kitware/trame-common/commit/87bcd3f579e994589a135122623942c809884a9a)) trame-common-1.0.1/LICENSE000066400000000000000000000261351504132732600151260ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. trame-common-1.0.1/README.md000066400000000000000000000062561504132732600154020ustar00rootroot00000000000000# trame-common: common code for any trame package Trame is rapidly evolving and having a dependency-less package to provide helper functions and classes across trame's eco-system is important for its future health. Initially some of those classes were created inside **trame-client**, **trame-server** or even **trame**, but we reached a point where some of those should become even more mainstream so they could easily be used on server, client, widget and more. That is where **trame-common** comes to play by providing a central location that any package can depend on. By default, **trame** remain the meta package that will impose some minimum version on **trame-common**, **trame-client** and **trame-server** and expose via some common namespace various pieces of those 3 dependencies. But if you need any piece of **trame-common**, feel free to depend on it. Trame-common is not meant to be installed by itself, but instead be used by any trame package that may require one of its function or helper class. While some of the module may require extra dependency, we are not listing them in this package purposely but the using code, should properly describe such dependency. ## Content **trame-common** is composed of several packages to split the current set of classes and function in meaningful groups. - **trame_common.assets**: Contains anything related to local and remote file including possible associated mime types. - **trame_common.decorators**: Contains all decorators for functions, classes and methods. - **trame_common.exec**: Contains helpers for handling code execution (i.e. async, throttle, debounce, thread, process). - **trame_common.obj**: Contains helpers for common trame objects (i.e. Component, App, Widget, Singleton) - **trame_common.utils**: Contains utility functions. ## License trame-common is made available under the Apache License, Version 2.0. For more details, see [LICENSE](https://github.com/Kitware/trame-common/blob/master/LICENSE). ## Development steps - Clone the repository using `git clone` - Install pre-commit via `pip install pre-commit` or `pip install -e ".[dev]"` - Run `pre-commit install` to set up pre-commit hooks - Run `pre-commit install --hook-type commit-msg` to register commit-msg hook - Make changes to the code, and commit your changes to a separate branch. Use [conventional commit messages](https://www.conventionalcommits.org/en/v1.0.0/). - Create a fork of the repository on GitHub - Push your branch to your fork, and open a pull request **Tips** - When first creating a new project, it is helpful to run `pre-commit run --all-files` to ensure all files pass the pre-commit checks. - A quick way to fix `ruff` issues is by installing ruff (`pip install ruff`) and running the `ruff check --fix .` or `ruff format` command at the root of your repository. - A quick way to fix `codespell` issues is by installing codespell (`pip install codespell`) and running the `codespell -w` command at the root of your directory. - The `.codespellrc file `\_ can be used fix any other codespell issues, such as ignoring certain files, directories, words, or regular expressions. trame-common-1.0.1/noxfile.py000066400000000000000000000016151504132732600161330ustar00rootroot00000000000000import shutil from pathlib import Path import nox DIR = Path(__file__).parent.resolve() nox.needs_version = ">=2024.3.2" nox.options.sessions = ["lint", "tests"] nox.options.default_venv_backend = "uv|virtualenv" @nox.session def lint(session: nox.Session) -> None: """ Run the linter. """ session.install("pre-commit") session.run( "pre-commit", "run", "--all-files", "--show-diff-on-failure", *session.posargs ) @nox.session def tests(session: nox.Session) -> None: """ Run the unit and regular tests. """ session.install(".[test]") session.run("pytest", *session.posargs) @nox.session def build(session: nox.Session) -> None: """ Build an SDist and wheel. """ build_path = DIR.joinpath("build") if build_path.exists(): shutil.rmtree(build_path) session.install("build") session.run("python", "-m", "build") trame-common-1.0.1/pyproject.toml000066400000000000000000000045101504132732600170260ustar00rootroot00000000000000[project] name = "trame-common" version = "1.0.1" authors = [ { name = "Sebastien Jourdain", email = "sebastien.jourdain@kitware.com" }, ] description = "Dependency less classes and functions for trame" readme = "README.md" requires-python = ">=3.9" classifiers = [ "Development Status :: 1 - Planning", "Intended Audience :: Science/Research", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", "Topic :: Scientific/Engineering", ] dependencies = [ ] [project.optional-dependencies] test = [ "pytest >=6", "pytest-cov >=3", "nox", ] dev = [ "pytest >=6", "pytest-cov >=3", "pre-commit", "ruff", "nox", ] [build-system] requires = ["hatchling"] build-backend = "hatchling.build" [tool.hatch.build] include = [ "/src/trame_common/**/*.py", ] [tool.hatch.build.targets.wheel] packages = ["src/trame_common"] [tool.uv] dev-dependencies = [ "trame-common[dev]", ] [tool.ruff] [tool.ruff.lint] extend-select = [ "ARG", # flake8-unused-arguments "B", # flake8-bugbear "C4", # flake8-comprehensions "EM", # flake8-errmsg "EXE", # flake8-executable "G", # flake8-logging-format "I", # isort "ICN", # flake8-import-conventions "NPY", # NumPy specific rules "PD", # pandas-vet "PGH", # pygrep-hooks "PIE", # flake8-pie "PL", # pylint "PT", # flake8-pytest-style "PTH", # flake8-use-pathlib "RET", # flake8-return "RUF", # Ruff-specific "SIM", # flake8-simplify "T20", # flake8-print "UP", # pyupgrade "YTT", # flake8-2020 ] ignore = [ "PLR09", # Too many <...> "PLR2004", # Magic value used in comparison "ISC001", # Conflicts with formatter ] isort.required-imports = [] [tool.ruff.lint.per-file-ignores] "tests/**" = ["T20"] "noxfile.py" = ["T20"] [tool.semantic_release] version_toml = [ "pyproject.toml:project.version", ] version_variables = [ "src/trame_common/__init__.py:__version__", ] build_command = """ python -m venv .venv source .venv/bin/activate pip install -U pip build python -m build . """ [tool.semantic_release.publish] dist_glob_patterns = ["dist/*"] upload_to_vcs_release = true trame-common-1.0.1/src/000077500000000000000000000000001504132732600147015ustar00rootroot00000000000000trame-common-1.0.1/src/trame_common/000077500000000000000000000000001504132732600173615ustar00rootroot00000000000000trame-common-1.0.1/src/trame_common/__init__.py000066400000000000000000000000261504132732600214700ustar00rootroot00000000000000__version__ = "1.0.1" trame-common-1.0.1/src/trame_common/assets/000077500000000000000000000000001504132732600206635ustar00rootroot00000000000000trame-common-1.0.1/src/trame_common/assets/__init__.py000066400000000000000000000000001504132732600227620ustar00rootroot00000000000000trame-common-1.0.1/src/trame_common/assets/local.py000066400000000000000000000102651504132732600223330ustar00rootroot00000000000000import base64 from pathlib import Path from trame_common.assets.mimetypes import to_mime def to_txt(full_path): """ Return the text content of the file path :param file_path: Path to the file to read :type file_path: str :return: File content :rtype: str """ return Path(full_path).read_text() def to_base64(full_path): """ Return the base64 content of the file path :param file_path: Path to the file to read :type file_path: str :return: File content encoded in base64 :rtype: str """ return base64.b64encode(Path(full_path).read_bytes()).decode("ascii") def to_url(full_path): """ Return the base64 encoded URL of the file path :param file_path: Path to the file to read :type file_path: str :return: Inlined bas64 encoded url (data:{mime};base64,{content}) :rtype: str """ encoded = to_base64(full_path) mime = to_mime(full_path) return f"data:{mime};base64,{encoded}" class LocalFileManager: """LocalFileManager provide convenient methods for handling local files""" def __init__(self, base_path): """ Provide the base path on which relative path should be based on. :param base_path: A file or directory path :type base_path: str """ _base = Path(base_path) # Ensure directory if _base.is_file(): _base = _base.parent self._root = Path(str(_base.resolve().absolute())) self._assests = {} def __getitem__(self, name): return self._assests.get(name) def __getattr__(self, name): return self._assests.get(name) def _to_path(self, file_path): _input_file = Path(file_path) if _input_file.is_absolute(): return str(_input_file.resolve().absolute()) return str(self._root.joinpath(file_path).resolve().absolute()) def base64(self, key, file_path=None): """ Store a base64 file content under the provided key name :param key: name for that content which can then be accessed by the [] or . notation :type key: str :param file_path: A file or directory path :type file_path: str :return: Return the stored content :rtype: str """ if file_path is None: file_path, key = key, file_path data = to_base64(self._to_path(file_path)) if key is not None: self._assests[key] = data return data def url(self, key, file_path): """ Store a url encoded file content under the provided key name :param key: name for that content which can then be accessed by the [] or . notation :type key: str :param file_path: A file or directory path :type file_path: str :return: Return the stored content :rtype: str """ if file_path is None: file_path, key = key, file_path data = to_url(self._to_path(file_path)) if key is not None: self._assests[key] = data return data def txt(self, key, file_path): """ Store a file content (text) under the provided key name :param key: name for that content which can then be accessed by the [] or . notation :type key: str :param file_path: A file or directory path :type file_path: str :return: Return the stored content :rtype: str """ if file_path is None: file_path, key = key, file_path data = to_txt(self._to_path(file_path)) if key is not None: self._assests[key] = data return data @property def assets(self): """Return the full set of assets as a dict""" return self._assests def get_assets(self, *keys): """Return a filtered out dict using the provided set of keys""" if len(keys) == 0: return self.assets _assets = {} for key in keys: _assets[key] = self._assests.get(key) return _assets __all__ = [ "LocalFileManager", "to_base64", "to_mime", "to_txt", "to_url", ] trame-common-1.0.1/src/trame_common/assets/mimetypes.py000066400000000000000000000024341504132732600232540ustar00rootroot00000000000000import mimetypes MIMETYPE_OVERRIDES = { # On Windows, mimetypes pulls this from the registry, which is # wrong. Override it. "application/javascript": ".js", } def decorate_mimetypes_init(): """ Decorate the mimetypes.init() method with our function that afterwards adds any mimetype overrides that were saved. """ original_init = mimetypes.init def new_init(*args, **kwargs): original_init(*args, **kwargs) for key, value in MIMETYPE_OVERRIDES.items(): mimetypes.add_type(key, value) mimetypes.init = new_init def add_mimetype_override(type, ext): """ Add a mimetype both now and when mimetypes.init() is called. :param type: mimetype :type type: str :param ext: file extension for which the mimetype applies :type ext: str """ # Add it to the overrides that are performed if init() is called MIMETYPE_OVERRIDES[type] = ext # Also override it right now mimetypes.add_type(type, ext) def to_mime(file_path): """ Return the mime type from a given path :param file_path: Path to analyse :type file_path: str :return: Mime type :rtype: str """ return mimetypes.guess_type(file_path)[0] # Decorate the mimetypes.init() method decorate_mimetypes_init() trame-common-1.0.1/src/trame_common/assets/remote.py000066400000000000000000000104521504132732600225320ustar00rootroot00000000000000from pathlib import Path from urllib.error import HTTPError from urllib.request import urlretrieve def download_file_from_google_drive(id, destination): import requests URL = "https://docs.google.com/uc" session = requests.Session() response = session.get( URL, params={ "id": id, "confirm": "t", "export": "download", }, stream=True, ) token = get_confirm_token(response) if token: params = {"id": id, "confirm": token} response = session.get(URL, params=params, stream=True) save_response_content(response, destination) def get_confirm_token(response): for key, value in response.cookies.items(): if key.startswith("download_warning"): return value return None def save_response_content(response, destination): CHUNK_SIZE = 32768 with Path(destination).open("wb") as f: for chunk in response.iter_content(CHUNK_SIZE): if chunk: # filter out keep-alive new chunks f.write(chunk) class AbstractRemoteFile: """ AbstractRemoteFile provide infrastructure for RemoteFile where only the method fetch() needs to be defined for a concreate implementation. """ def __init__(self, local_path=None, local_base=None): # setup base path self._base = Path.cwd().resolve() if local_base is not None: local_base = Path(local_base).resolve() self._base = local_base if local_base.exists() and local_base.is_file(): self._base = local_base.parent # setup local path self._file_path = Path(local_path) if not self._file_path.is_absolute(): self._file_path = self._base / local_path # Make sure target directory exists parent_dir = self._file_path.parent parent_dir.mkdir(parents=True, exist_ok=True) @property def local(self): """Return true if the file is available locally on the File System""" return self._file_path.exists() def fetch(self): """Perform the action needed to fetch the content and store it locally""" @property def path(self): """Return the actual local file path""" if not self.local: self.fetch() return str(self._file_path) class GoogleDriveFile(AbstractRemoteFile): """ Helper file to manage caching and retrieving of file available on Google Drive """ def __init__( self, local_path=None, google_id=None, local_base=None, ): """ Provide the information regarding where the file should be located and where to fetch it if missing. :param local_path: relative or absolute path :param google_id: Resource ID from google :param local_base: Absolute path when local_path is relative """ super().__init__(local_path, local_base) self._gid = google_id def fetch(self): try: print(f"Downloading:\n - {self._gid}\n - to {self._file_path}") # noqa: T201 download_file_from_google_drive(self._gid, self._file_path) except HTTPError as e: print(RuntimeError(f"Failed to download {self._gid}. {e.reason}")) # noqa: T201 class HttpFile(AbstractRemoteFile): """ Helper file to manage caching and retrieving of file available on HTTP servers """ def __init__( self, local_path=None, remote_url=None, local_base=None, ): """ Provide the information regarding where the file should be located and where to fetch it if missing. :param local_path: relative or absolute path :param remote_url: http(s):// url to fetch the file from :param local_base: Absolute path when local_path is relative """ super().__init__(local_path, local_base) self._url = remote_url def fetch(self): try: print(f"Downloading:\n - {self._url}\n - to {self._file_path}") # noqa: T201 urlretrieve(self._url, str(self._file_path)) except HTTPError as e: print(RuntimeError(f"Failed to download {self._url}. {e.reason}")) # noqa: T201 __all__ = [ "AbstractRemoteFile", "GoogleDriveFile", "HttpFile", ] trame-common-1.0.1/src/trame_common/decorators/000077500000000000000000000000001504132732600215265ustar00rootroot00000000000000trame-common-1.0.1/src/trame_common/decorators/__init__.py000066400000000000000000000000001504132732600236250ustar00rootroot00000000000000trame-common-1.0.1/src/trame_common/decorators/hot_reload.py000066400000000000000000000236451504132732600242320ustar00rootroot00000000000000"""Automatically reload function code each time it is called This module provides a `hot_reload` decorator where the decorated function is reloaded from source every time the function is called. This works primarily for global functions and class methods, but it also has limited support for nested functions as well*. This module also provides a `reload` function that can be used to reload a function on demand, i. e. `new_func = reload(old_func)`. This module is based upon Julian Vossen's reloading library: https://github.com/julvo/reloading But it is heavily modified, and includes additional support for things like class methods. The license for the original library is pasted at the bottom of this file. * Nested functions have the following issues: 1. You cannot use the `nonlocal` statement in nested functions. When the code is reloaded, Python treats it as a global function, and therefore the `nonlocal` keyword is not allowed. 2. You cannot add capture variables during reloading. Whatever was captured when the function was first defined is what will continue to be captured. This is because the outer function is never reloaded, and the outer function's local scope may potentially be gone at the time of decorating. 3. If some of the capture variables share the same name as global variables, but are different, then references to the global variable within the function or functions it calls may end up referring to the capture variable instead. """ import ast import functools import inspect import site import sys import traceback import types from pathlib import Path try: from trame_client.ui.core import AbstractLayout from trame_client.widgets.core import AbstractElement # Skip any methods whose classes inherit from these SKIP_CLASSES = [AbstractElement, AbstractLayout] except ImportError: SKIP_CLASSES = [] # Strip any decorators with these names STRIP_DECORATORS = ["ctrl", "state", "life_cycle"] # We don't actually have logic right now to reload lambdas anyways SKIP_LAMBDA_FUNCS = True # If this is True, then skip any functions that are located under any # site packages directories. # This essentially means to skip any functions that are not located # in editable environments. SKIP_SITE_PACKAGES = True def hot_reload(func): """Decorator to reload the function on every call If there are multiple decorators on this function, only the decorators after the `@hot_reload` decorator will be reloaded """ if getattr(func, "__is_hot_reload_func", False): # It is already a hot_reload function. Just return it. # This prevents multiple hot_reload wrappers. return func @functools.wraps(func) def wrapped(*args, **kwargs): # If the user decorated the function manually, make sure we # don't perform checks and reload every time. new_func = reload(func, perform_checks=False) return new_func(*args, **kwargs) wrapped.__is_hot_reload_func = True return wrapped def reload(func, perform_checks=True): """Attempt to reload the provided function This works by locating the file the function was defined in, reloading its body of code, and returning the reloaded function. If perform_checks is True, then several checks will be performed beforehand to determine whether or not the function should be skipped. """ if perform_checks: if not isinstance(func, (types.FunctionType, types.MethodType)): # We only allow reloading of function and method types return func if isinstance(func, types.MethodType) and isinstance( func.__self__, tuple(SKIP_CLASSES) ): # We need to skip reloading methods on this class return func if SKIP_LAMBDA_FUNCS and "" in func.__qualname__: # Skip lambda functions. It is harder to reload them. return func if SKIP_SITE_PACKAGES and _func_in_site_packages(func): # Skip any packages that were not installed in editable mode return func while True: try: return _reload_func(func) except Exception: _handle_exception(func) def _reload_func(func): func_locals = _find_function_locals(func) code = _recompile_function(func) # Unfortunately, exec is a little challenging here for non-global # functions. # Since the source function is compiled by itself, it is treated as # a global function, and therefore cannot have closure variables. # Because of this, the function will not have read access to the locals # we pass in (even though it can still write the function to the locals). # To work around this, we copy the locals to the globals (and some globals # may be over-written as a result). # An alternative work-around would be to add AST code to put a small # wrapper function around the function before compiling, such as: # def _wrapper(): # # _wrapper() # This may get the function to capture the local variables. globals_copy = func.__globals__.copy() globals_copy.update(func_locals) exec(code, globals_copy) new_func = globals_copy[func.__name__] if isinstance(func, types.MethodType): # This is a bound method. Make the new method bound as well. new_func = types.MethodType(new_func, func.__self__) return new_func def _find_function_locals(func): # First, look at the qualified name. # If is not in the qualified name, then the locals should be # the same as the globals. This is much easier for reloading. if "" not in func.__qualname__: return func.__globals__ # If is in the qualified name, then we may need captured closure # variables to run the function correctly. We will use these as the locals. # Note that this means that new closure variables cannot be added when # reloading the function. return inspect.getclosurevars(func).nonlocals def _recompile_function(func): tree = _parse_func_file_until_successful(func) if not _isolate_function_def(func.__name__, tree): path = inspect.getfile(func) msg = f"Failed to find '{func.__qualname__}' in file '{path}'" raise Exception(msg) return compile(tree, filename="", mode="exec") def _parse_func_file_until_successful(func): path = inspect.getfile(func) while True: source = _load_file(path) try: return ast.parse(source) except SyntaxError: _handle_exception(func) def _load_file(path): src = "" # while loop here since while saving, the file may sometimes be empty. while src == "": src = Path(path).read_text() return src + "\n" def _isolate_function_def(funcname, tree): """Strip everything but the function definition from the ast in-place. Also strips the hot_reload decorator (including all decorators before it) from the function definition""" for node in ast.walk(tree): if isinstance(node, ast.FunctionDef) and node.name == funcname: decorator_names = [_get_decorator_name(dec) for dec in node.decorator_list] if "hot_reload" in decorator_names: _strip_hot_reload_decorator(node) # Remove any decorators that we have indicated to strip _strip_decorators(node, STRIP_DECORATORS) tree.body = [node] return True return False def _handle_exception(func): fpath = inspect.getfile(func) exc = traceback.format_exc() exc = exc.replace('File ""', f'File "{fpath}"') sys.stderr.write(exc + "\n") print(f"Edit '{func.__qualname__}' in '{fpath}' and press return to continue") # noqa: T201 sys.stdin.readline() def _get_decorator_name(dec_node): if hasattr(dec_node, "id"): return dec_node.id if hasattr(dec_node, "func"): if hasattr(dec_node.func, "id"): return dec_node.func.id if hasattr(dec_node.func, "value"): return dec_node.func.value.id if hasattr(dec_node, "value"): return dec_node.value.id msg = f"Failed to find decorator name: {dec_node}" raise Exception(msg) def _strip_hot_reload_decorator(func): """Remove the 'hot_reload' decorator and all decorators before it""" decorator_names = [_get_decorator_name(dec) for dec in func.decorator_list] hot_reload_idx = decorator_names.index("hot_reload") func.decorator_list = func.decorator_list[hot_reload_idx + 1 :] def _strip_decorators(func, blacklist): """Strip only specific decorators""" func.decorator_list = [ dec for dec in func.decorator_list if _get_decorator_name(dec) not in blacklist ] def _func_in_site_packages(func): """Return whether a function is in any site-packages directories""" path = inspect.getfile(func) return any(path.startswith(x) for x in site.getsitepackages()) JULVO_RELOADING_LICENSE = """MIT License Copyright (c) 2019 Julian Vossen 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. """ trame-common-1.0.1/src/trame_common/decorators/klass.py000066400000000000000000000133541504132732600232230ustar00rootroot00000000000000import functools import inspect import logging logger = logging.getLogger(__name__) # logging.basicConfig(level=logging.DEBUG) def can_be_decorated(x): return inspect.ismethod(x) or inspect.isfunction(x) class TrameApp: """ Class Decorator for trame application. This decorator can be used to automatically register methods to state.change, controller, trigger and life_cycle. The parameter are used to define where to fine the server instance within the object and where a namespace for the variable name is stored. .. code-block:: python @TrameApp() class ExampleApp: def __init__(self): self.server = get_server() @change("var_name_1", "var_name_n") def on_state_change(**kwargs): pass @controller.set("hello") def hello(**kwargs): pass """ def __init__(self, server="server", namespace=""): self.server_name = server self.namespace_prefix = namespace def __call__(self, klass): self.klass_init = klass.__init__ @functools.wraps(klass.__init__, updated=()) def wrapped_init(instance, *args, **kwargs): logger.debug("Create instance") self.klass_init(instance, *args, **kwargs) logger.debug("Instance created") server = getattr(instance, self.server_name) prefix = ( getattr(instance, self.namespace_prefix) if self.namespace_prefix else "" ) logger.debug("server=%s prefix=%s", server, prefix) # Look for method decorator for k in inspect.getmembers(instance.__class__, can_be_decorated): fn = getattr(instance, k[0]) # Handle @state.change if "_trame_state_change" in fn.__dict__: state_change_names = fn.__dict__["_trame_state_change"] key_names = [f"{prefix}{v}" for v in state_change_names] logger.debug("state.change(%s)(%s)", key_names, k[0]) server.state.change(*key_names)(fn) # Handle @trigger if "_trame_trigger_names" in fn.__dict__: trigger_names = fn.__dict__["_trame_trigger_names"] for trigger_name in trigger_names: logger.debug("trigger(%s)(%s)", trigger_name, k[0]) server.trigger(f"{trigger_name}")(fn) if prefix: logger.debug( "trigger(%s%s)(%s)", prefix, trigger_name, k[0] ) server.trigger(f"{prefix}{trigger_name}")(fn) # Handle @ctrl.[add, once, add_task, set] if "_trame_controller" in fn.__dict__: actions = fn.__dict__["_trame_controller"] for action in actions: name = action.get("name") method = action.get("method") decorate = getattr(server.controller, method) logger.debug("ctrl.%s(%s)(%s)", method, name, k[0]) decorate(name)(fn) if prefix: logger.debug( "ctrl.%s(%s%s)(%s)", method, prefix, name, k[0] ) decorate(f"{prefix}{name}")(fn) klass.__init__ = wrapped_init return klass def change(*args): """Method decorator for state change""" def decorate(f): if not hasattr(f, "_trame_state_change"): f._trame_state_change = [] f._trame_state_change.extend(args) return f return decorate def trigger(*args): """Method decorator to assign a trigger name to a function""" def decorate(f): if not hasattr(f, "_trame_trigger_names"): f._trame_trigger_names = [] f._trame_trigger_names.extend(args) return f return decorate def controller_decorator(method): def decorator(*args): def decorate(f): if not hasattr(f, "_trame_controller"): f._trame_controller = [] for name in args: f._trame_controller.append({"method": method, "name": name}) return f return decorate return decorator class Controller: """Controller decorators .. code-block:: text - once - add - add_task - set """ def __init__(self): self.once = controller_decorator("once") self.add = controller_decorator("add") self.add_task = controller_decorator("add_task") self.set = controller_decorator("set") class LifeCycle: """Life Cycle decorators .. code-block:: text - server_start - server_bind - server_ready - client_connected - client_exited - server_exited - server_reload """ def __init__(self): self.server_start = controller_decorator("add")("on_server_start") self.server_bind = controller_decorator("add")("on_server_bind") self.server_ready = controller_decorator("add")("on_server_ready") self.client_connected = controller_decorator("add")("on_client_connected") self.client_exited = controller_decorator("add")("on_client_exited") self.server_exited = controller_decorator("add")("on_server_exited") self.server_reload = controller_decorator("add")("on_server_reload") controller = Controller() life_cycle = LifeCycle() __all__ = [ "TrameApp", "change", "controller", "life_cycle", "trigger", ] trame-common-1.0.1/src/trame_common/exec/000077500000000000000000000000001504132732600203055ustar00rootroot00000000000000trame-common-1.0.1/src/trame_common/exec/__init__.py000066400000000000000000000000001504132732600224040ustar00rootroot00000000000000trame-common-1.0.1/src/trame_common/exec/asynchronous.py000066400000000000000000000030421504132732600234110ustar00rootroot00000000000000import asyncio import logging __all__ = [ "create_task", "decorate_task", "handle_task_result", "task", ] def handle_task_result(task: asyncio.Task) -> None: try: task.result() except asyncio.CancelledError: pass # Task cancellation should not be logged as an error. except Exception: # pylint: disable=broad-except logging.exception("Exception raised by task = %r", task) def decorate_task(task): """ Decorate a task by attaching a done callback so any exception or error could be caught and reported. :param task: A coroutine to execute as an independent task :type task: asyncio.Task :return: The same task object :rtype: asyncio.Task """ task.add_done_callback(handle_task_result) return task def create_task(coroutine, loop=None): """ Create a task from a coroutine while also attaching a done callback so any exception or error could be caught and reported. :param coroutine: A coroutine to execute as an independent task :param loop: Optionally provide the loop on which the task should be scheduled on. By default we will use the current running loop. :return: The decorated task :rtype: asyncio.Task """ if loop is None: loop = asyncio.get_event_loop() return decorate_task(loop.create_task(coroutine)) def task(func): """Function decorator to make its async execution within a task""" def wrapper(*args, **kwargs): create_task(func(*args, **kwargs)) return wrapper trame-common-1.0.1/src/trame_common/exec/throttle.py000066400000000000000000000033231504132732600225250ustar00rootroot00000000000000import asyncio class Throttle: """ Helper class that wrap a function with a given max execution rate. By default the rate is set to execute no more than once a second. :param fn: the function to call. :type fn: function :param ts: Number of seconds to wait before the next execution. :type ts: float """ def __init__(self, fn, ts=1): self._ts = ts self._fn = fn self._requests = 0 self._pending = False self._pending_task = None self._last_args = [] self._last_kwargs = {} @property def rate(self): """Number of maximum executions per second""" return 1.0 / self._ts @rate.setter def rate(self, rate): """Update the maximum number of executions per seconds""" self._ts = 1.0 / rate @property def delta_t(self): """Number of seconds to wait between execution""" return self._ts @delta_t.setter def delta_t(self, seconds): """Update the number of seconds to wait between execution""" self._ts = seconds async def _trottle(self): self._pending = True if self._requests: self._fn(*self._last_args, **self._last_kwargs) self._requests = 0 await asyncio.sleep(self._ts) if self._requests > 0: await self._trottle() self._pending = False def __call__(self, *args, **kwargs): """Function call wrapper that will throttle the actual function provided at construction""" self._requests += 1 self._last_args = args self._last_kwargs = kwargs if not self._pending: self._pending_task = asyncio.create_task(self._trottle()) trame-common-1.0.1/src/trame_common/obj/000077500000000000000000000000001504132732600201335ustar00rootroot00000000000000trame-common-1.0.1/src/trame_common/obj/__init__.py000066400000000000000000000000001504132732600222320ustar00rootroot00000000000000trame-common-1.0.1/src/trame_common/obj/app.py000066400000000000000000000014661504132732600212740ustar00rootroot00000000000000from trame.app import get_server from trame_common.exec.asynchronous import create_task from trame_common.obj.component import TrameComponent class TrameApp(TrameComponent): """ Base trame class that has access to a trame server instance on which we provide simple accessor and method decoration capabilities. """ def __init__(self, server=None, client_type="vue3", ctx_name=None, **_): super().__init__(get_server(server, client_type=client_type), ctx_name=ctx_name) async def _async_display(self): from IPython.display import clear_output await self.ui.ready clear_output(wait=True) self.ui._ipython_display_() def _repr_html_(self): create_task(self._async_display()) return "Launching trame server in the background..." trame-common-1.0.1/src/trame_common/obj/component.py000066400000000000000000000066211504132732600225140ustar00rootroot00000000000000import inspect import logging __all__ = [ "TrameComponent", ] logger = logging.getLogger(__name__) def can_be_decorated(x): return inspect.ismethod(x) or inspect.isfunction(x) class TrameComponent: """ Base trame class that has access to a trame server instance on which we provide simple accessor and method decoration capabilities. """ def __init__(self, server, ctx_name=None, **_): """ Initialize TrameComponent with its server. Keyword arguments: server -- the server to link to (default None) ctx_name -- name to use to bind current instance to server.context (default None) """ self._server = server if ctx_name: self.ctx[ctx_name] = self self._bind_annotated_methods() @property def server(self): """Return the associated trame server instance""" return self._server @property def state(self): """Return the associated server state""" return self.server.state @property def ctrl(self): """Return the associated server controller""" return self.server.controller @property def ctx(self): """Return the associated server context""" return self.server.context def _bind_annotated_methods(self): # Look for method decorator for k in inspect.getmembers(self.__class__, can_be_decorated): fn = getattr(self, k[0]) # Handle @state.change s_translator = self.state.translator if "_trame_state_change" in fn.__dict__: state_change_names = fn.__dict__["_trame_state_change"] logger.debug( "state.change(%s)(%s)", [f"{s_translator.translate_key(v)}" for v in state_change_names], k[0], ) self.state.change(*[f"{v}" for v in state_change_names])(fn) # Handle @trigger if "_trame_trigger_names" in fn.__dict__: trigger_names = fn.__dict__["_trame_trigger_names"] for trigger_name in trigger_names: logger.debug("trigger(%s)(%s)", trigger_name, k[0]) self.server.trigger(f"{trigger_name}")(fn) # Handle @ctrl.[add, once, add_task, set] if "_trame_controller" in fn.__dict__: actions = fn.__dict__["_trame_controller"] for action in actions: name = action.get("name") method = action.get("method") decorate = getattr(self.ctrl, method) logger.debug("ctrl.%s(%s)(%s)", method, name, k[0]) decorate(name)(fn) def _unbind_annotated_methods(self): # Look for method decorator for k in inspect.getmembers(self.__class__, can_be_decorated): fn = getattr(self, k[0]) # Handle @state.change methods_to_detach = {} if "_trame_state_change" in fn.__dict__: methods_to_detach.add(fn) if methods_to_detach: for fn_list in self.state._change_callbacks.values(): to_remove = set(fn_list) | methods_to_detach for fn in to_remove: fn_list.remove(fn) # Handle @trigger # TODO # Handle @ctrl # TODO trame-common-1.0.1/src/trame_common/obj/singleton.py000066400000000000000000000010731504132732600225100ustar00rootroot00000000000000from typing import Generic, TypeVar T = TypeVar("T") class Singleton(Generic[T]): """ Class decorator to make it a Singleton This is useful when you want a central engine instance or else to be used across your application modules. But using such decorator will make it tricky or impossible to use your application within several server using the same event loop. """ def __init__(self, cls: type[T]): self._instance: T = cls() def __call__(self) -> T: return self._instance __all__ = [ "Singleton", ] trame-common-1.0.1/src/trame_common/obj/widget.py000066400000000000000000000020771504132732600217760ustar00rootroot00000000000000from trame_client.widgets.core import AbstractElement __all__ = [ "create_class", ] def create_class( class_name, component_name, properties=None, events=None, module=None, ): """Helper for creating Widget class Args: class_name (string): name of the Python generated class component_name (string): name of the vue component properties (list, optional): List of properties mapping. Defaults to []. events (list, optional): List of events mapping. Defaults to []. module (dict, optional): Module to enable when using the class. Defaults to None. """ if properties is None: properties = [] if events is None: events = [] def constructor(self, **kwargs): AbstractElement.__init__(self, component_name, **kwargs) if module is not None: self.server.enable_module(module) self._attr_names += properties self._event_names += events return type( class_name, (AbstractElement,), {"__init__": constructor}, ) trame-common-1.0.1/src/trame_common/utils/000077500000000000000000000000001504132732600205215ustar00rootroot00000000000000trame-common-1.0.1/src/trame_common/utils/__init__.py000066400000000000000000000000001504132732600226200ustar00rootroot00000000000000trame-common-1.0.1/src/trame_common/utils/version.py000066400000000000000000000011611504132732600225570ustar00rootroot00000000000000try: # Use importlib metadata if available (python >=3.8) from importlib.metadata import PackageNotFoundError, version except ImportError: # No importlib metadata. Try to use pkg_resources instead. from pkg_resources import ( DistributionNotFound as PackageNotFoundError, ) from pkg_resources import ( get_distribution, ) def version(x): return get_distribution(x).version __all__ = [ "get_version", ] def get_version(package_name): try: return version(package_name) except PackageNotFoundError: # package is not installed pass trame-common-1.0.1/tests/000077500000000000000000000000001504132732600152545ustar00rootroot00000000000000trame-common-1.0.1/tests/test_packages.py000066400000000000000000000002211504132732600204360ustar00rootroot00000000000000import importlib.metadata import trame_common as m def test_version(): assert importlib.metadata.version("trame_common") == m.__version__