{"id":41980213,"url":"https://github.com/globus/together","last_synced_at":"2026-01-25T23:41:05.772Z","repository":{"id":57476083,"uuid":"327138788","full_name":"globus/together","owner":"globus","description":"[Experimental] Build CLIs which \"click\" together using pluggy","archived":false,"fork":false,"pushed_at":"2021-01-22T17:44:30.000Z","size":48,"stargazers_count":4,"open_issues_count":0,"forks_count":1,"subscribers_count":3,"default_branch":"main","last_synced_at":"2024-11-18T00:47:51.647Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/globus.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2021-01-05T22:47:45.000Z","updated_at":"2023-05-10T08:58:21.000Z","dependencies_parsed_at":"2022-09-07T14:10:31.937Z","dependency_job_id":null,"html_url":"https://github.com/globus/together","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/globus/together","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/globus%2Ftogether","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/globus%2Ftogether/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/globus%2Ftogether/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/globus%2Ftogether/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/globus","download_url":"https://codeload.github.com/globus/together/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/globus%2Ftogether/sbom","scorecard":{"id":430120,"data":{"date":"2025-08-11","repo":{"name":"github.com/globus/together","commit":"2cf30a6ffd084528c0446987c2427a472115bc54"},"scorecard":{"version":"v5.2.1-40-gf6ed084d","commit":"f6ed084d17c9236477efd66e5b258b9d4cc7b389"},"score":2.4,"checks":[{"name":"Pinned-Dependencies","score":0,"reason":"dependency not pinned by hash detected -- score normalized to 0","details":["Warn: pipCommand not pinned by hash: examples/multipackage/install.sh:12","Warn: pipCommand not pinned by hash: examples/multipackage/install.sh:13","Info:   0 out of   2 pipCommand dependencies pinned"],"documentation":{"short":"Determines if the project has declared and pinned the dependencies of its build process.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#pinned-dependencies"}},{"name":"Binary-Artifacts","score":10,"reason":"no binaries found in the repo","details":null,"documentation":{"short":"Determines if the project has generated executable (binary) artifacts in the source repository.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#binary-artifacts"}},{"name":"Code-Review","score":0,"reason":"Found 0/12 approved changesets -- score normalized to 0","details":null,"documentation":{"short":"Determines if the project requires human code review before pull requests (aka merge requests) are merged.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#code-review"}},{"name":"Packaging","score":-1,"reason":"packaging workflow not detected","details":["Warn: no GitHub/GitLab publishing workflow detected."],"documentation":{"short":"Determines if the project is published as a package that others can easily download, install, easily update, and uninstall.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#packaging"}},{"name":"Dangerous-Workflow","score":-1,"reason":"no workflows found","details":null,"documentation":{"short":"Determines if the project's GitHub Action workflows avoid dangerous patterns.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#dangerous-workflow"}},{"name":"Token-Permissions","score":-1,"reason":"No tokens found","details":null,"documentation":{"short":"Determines if the project's workflows follow the principle of least privilege.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#token-permissions"}},{"name":"Maintained","score":0,"reason":"0 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 0","details":null,"documentation":{"short":"Determines if the project is \"actively maintained\".","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#maintained"}},{"name":"SAST","score":0,"reason":"no SAST tool detected","details":["Warn: no pull requests merged into dev branch"],"documentation":{"short":"Determines if the project uses static code analysis.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#sast"}},{"name":"CII-Best-Practices","score":0,"reason":"no effort to earn an OpenSSF best practices badge detected","details":null,"documentation":{"short":"Determines if the project has an OpenSSF (formerly CII) Best Practices Badge.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#cii-best-practices"}},{"name":"Vulnerabilities","score":10,"reason":"0 existing vulnerabilities detected","details":null,"documentation":{"short":"Determines if the project has open, known unfixed vulnerabilities.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#vulnerabilities"}},{"name":"License","score":0,"reason":"license file not detected","details":["Warn: project does not have a license file"],"documentation":{"short":"Determines if the project has defined a license.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#license"}},{"name":"Fuzzing","score":0,"reason":"project is not fuzzed","details":["Warn: no fuzzer integrations found"],"documentation":{"short":"Determines if the project uses fuzzing.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#fuzzing"}},{"name":"Signed-Releases","score":-1,"reason":"no releases found","details":null,"documentation":{"short":"Determines if the project cryptographically signs release artifacts.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#signed-releases"}},{"name":"Branch-Protection","score":0,"reason":"branch protection not enabled on development/release branches","details":["Warn: branch protection not enabled for branch 'main'"],"documentation":{"short":"Determines if the default and release branches are protected with GitHub's branch protection settings.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#branch-protection"}},{"name":"Security-Policy","score":0,"reason":"security policy file not detected","details":["Warn: no security policy file detected","Warn: no security file to analyze","Warn: no security file to analyze","Warn: no security file to analyze"],"documentation":{"short":"Determines if the project has published a security policy.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#security-policy"}}]},"last_synced_at":"2025-08-19T03:08:26.041Z","repository_id":57476083,"created_at":"2025-08-19T03:08:26.041Z","updated_at":"2025-08-19T03:08:26.041Z"},"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28761816,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-25T23:06:19.311Z","status":"ssl_error","status_checked_at":"2026-01-25T23:03:50.555Z","response_time":113,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-01-25T23:41:04.901Z","updated_at":"2026-01-25T23:41:05.759Z","avatar_url":"https://github.com/globus.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# together\n\nBuild CLIs which `click` `together` using `pluggy`.\n\n\u003e **WARNING**: Experimental. This module is an initial proof of concept and\n\u003e may change significantly.\n\n## Usage\n\nDefine a TogetherCLI by registering plugins which register either click.Group\nor click.Command objects. Then, build and invoke the CLI. See `examples/` for\ncomplete examples of standalone and multi-package usage.\n\n### Minimal Usage\n\nPossibly the smallest usage example possible defines a single command with a\nsingle subcommand in one file. Each plugin is implemented as a class (though\nimplementing plugins as modules is also supported).\n\n```python\n# in mycli.py\nimport together\n\n\nclass BasePlugin:\n    @together.hook\n    def together_root_command(self, config):\n        @click.group(\"mycli\")\n        def mycli():\n            pass\n\n        return mycli\n\n\nclass FooPlugin:\n    @together.hook\n    def together_subcommand(self, config):\n        @click.command(\"foo\")\n        def foo():\n            click.echo(\"running foo\")\n\n        return foo\n\n\nclass MyCLI(together.TogetherCLI):\n    # customize plugin loading to specify these plugins\n    def register_plugins(self):\n        self.plugin_manager.register(BasePlugin())\n        self.plugin_manager.register(FooPlugin())\n\n\n# build the CLI\nrunme = MyCLI()\n# run it\nrunme()\n```\n\nThis would allow usage like `python mycli.py foo`.\n\n### Changing Plugin Loading\n\nBy default, plugins are loaded by looking up the `together` setuptools\nentrypoint. To define custom plugin loading, override the\n`TogetherCLI.register_plugins` method in a subclass.\n\nFor example, you could load a mix of explicit plugins and setuptools\nentrypoints with a custom name like so:\n\n```python\nimport mypackage.plugins\n\nclass MyCLI(together.TogetherCLI):\n    def register_plugins(self):\n        # self.plugin_manager is a pluggy.PluginManager instance\n        self.plugin_manager.register(mypackage.plugins.Foo)\n        self.plugin_manager.register(mypackage.plugins.Bar)\n        self.plugin_manager.load_setuptools_entrypoints(\"mypackage\")\n```\n\n### Using CommandState\n\n`together` automatically defines a class, `together.CommandState` which\nimplements stateful tracking of option data as it is parsed. This allows the\nvalue which was passed to a parent command or the root command to be recorded\non the click context and accessed by child command callbacks.\n\nFor example, if you wish to support arguments like `--format=[json|text]` in\nyour commands, and allow usage like `mycli --format=json foo` to be valid, you\nneed to save and retrieve that option. `CommandState` implements this logic and\nis automatically registered as the context object for commands.\n\nTo implement such an option, define a click decorator which uses a callback to\nget the state object and use it for storage. You can also write an easy getter\nfunction to get back the stored value. Like so:\n\n```python\nimport click\nfrom together import get_state\n\ndef format_option(f):\n    def callback(ctx, param, value):\n        state = get_state()\n        state.set(\"format\", value)\n    return click.option(\n        \"--format\", type=click.Choice([\"json\", \"text\"]), callback=callback\n    )(f)\n\n\ndef get_format():\n    return get_state().get(\"format\")\n```\n\n\u003e **NOTE**: `get_state()` requires that there is an active Click Context\n\u003e it therefore can only normally execute inside of running Click commands\n\n#### Using CommandState to access Config\n\nThe configuration object produced by your `together_configure` hooks is\nattached to the CommandState for easy access. All you need to do is get the\nstate object and look at its `config` attribute:\n\n```python\ndef was_configured_to_foobar():\n    state = get_state()\n    return state.config.get(\"foobar\") is True\n```\n\n## Plugin Order and Execution\n\nSeveral rules govern how plugins execute and their ordering.\n\n1. Plugins are `pluggy` plugins and hooks _execute_ in LIFO order\n2. `together` reverses the order of the resulting registrations to produce FIFO ordering\n3. Only the last plugin to register a root command will execute. You should\n   only register one root command.\n\nFor the most part, this will make the plugin subcommand registration operate in\nFIFO order.\n\n## CHANGELOG\n\n### 0.5.2\n\n* Fix packaging bug\n\n### 0.5.1\n\n* Convert from `poetry` to simple `setup.cfg` data\n\n### 0.5.0\n\n* Add the `together_exception_handler` hook for registering exception handlers\n  which can be matched against errors when invoking the CLI app\n * Exception handlers combine exception matching functions (predicates),\n   exception handling callbacks, and optional priority levels (to ensure early\n   or late matching)\n * Predicates may be boolean functions or exception classes, which will be\n   checked with `isinstance`\n\n* You can (and should) now invoke a TogetherCLI by calling it, not relying upon\n    the `build()` function output\n\n### 0.4.0\n\n* Add the CommandState object supporting arbitrary storage (in an inner dict)\n  and the `get_state` helper\n\n* Add `verbose_option` and `get_verbosity` built on the CommandState object\n\nThis can be used to implement the common pattern of defining a single central\nobject which is pulled off of the click context to record options which might\nbe valid at multiple levels of a command heirarchy.\n\n### 0.3.1\n\n* Bugfix: incorrectly unpacking subcommand lists\n\n### 0.3.0\n\n* Replace `together_subcommand_collection` with support in the\n  `together_subcommand` hook for lists of subcommands\n\n### 0.2.1\n\n* Allow hooks to return command registration info without explicitly wrapping\n  it in a registration object\n\n### 0.2.0\n\n* Change hook names and support config\n\n### 0.1.0\n\n* Initial release\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fglobus%2Ftogether","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fglobus%2Ftogether","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fglobus%2Ftogether/lists"}