{"id":20969384,"url":"https://github.com/cubicpath/dyncommands","last_synced_at":"2025-03-13T08:13:24.657Z","repository":{"id":57424835,"uuid":"435058986","full_name":"Cubicpath/dyncommands","owner":"Cubicpath","description":"Dynamic command execution, parsing, and storage for IRC chatbots and CLI applications.","archived":false,"fork":false,"pushed_at":"2023-07-10T21:54:18.000Z","size":120,"stargazers_count":1,"open_issues_count":2,"forks_count":0,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-02-20T03:03:52.317Z","etag":null,"topics":["dynamic","extensible","library","parser","pypi-package","python","python3"],"latest_commit_sha":null,"homepage":"https://pypi.org/project/dyncommands/","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/Cubicpath.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2021-12-05T02:53:10.000Z","updated_at":"2022-03-12T09:17:49.000Z","dependencies_parsed_at":"2022-09-10T03:53:40.252Z","dependency_job_id":null,"html_url":"https://github.com/Cubicpath/dyncommands","commit_stats":null,"previous_names":[],"tags_count":7,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Cubicpath%2Fdyncommands","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Cubicpath%2Fdyncommands/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Cubicpath%2Fdyncommands/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Cubicpath%2Fdyncommands/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Cubicpath","download_url":"https://codeload.github.com/Cubicpath/dyncommands/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":243365641,"owners_count":20279215,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","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":["dynamic","extensible","library","parser","pypi-package","python","python3"],"created_at":"2024-11-19T03:21:03.713Z","updated_at":"2025-03-13T08:13:24.508Z","avatar_url":"https://github.com/Cubicpath.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"dyncommands\n===============\nDynamic command execution, parsing, and storage.\n\n------------------------------\n\n[![Tests](https://img.shields.io/github/workflow/status/Cubicpath/dyncommands/Tests?logo=github\u0026style=for-the-badge)][unittests]\n[![Codecov](https://img.shields.io/codecov/c/gh/Cubicpath/dyncommands?label=Coverage\u0026logo=codecov\u0026style=for-the-badge)][coverage]\n[![MIT License](https://img.shields.io/github/license/Cubicpath/dyncommands?style=for-the-badge)][license]\n\n[![PyPI](https://img.shields.io/pypi/v/dyncommands?label=PyPI\u0026logo=pypi\u0026style=flat-square)][homepage]\n[![Python](https://img.shields.io/pypi/pyversions/dyncommands?label=Python\u0026logo=python\u0026style=flat-square)][python]\n[![CPython](https://img.shields.io/pypi/implementation/dyncommands?label=Impl\u0026logo=python\u0026style=flat-square)][python]\n\n------------------------------\n\nAbout:\n---------------\nDyncommands allows you to dynamically import and run python functions. Useful for adding commands to IRC chatbots or CLI applications without a restart.\n\nWhen parsing a string, it separates the command name from arguments, and executes the stored function with those arguments.\nEach time the parser is called, you can pass in your own custom kwargs that the command will have access to.\n\nAll command modules are compiled through [RestrictedPython] before being allowed to run.\nYou can turn off Restricted execution by setting `CommandParser._unrestricted` to _true_, though this is highly discouraged when running untrusted code.\n\nHow to use:\n---------------\n\n### Short example:\n\n```python\nfrom pathlib import Path\nfrom dyncommands import CommandParser, CommandContext, CommandSource\n\noutput: str = ''\n\ndef callback(text, *args):\n    global output\n    output = text\n\npath = Path('path/to/directory')  # Must be a directory with a `commands.json` file in it\nparser = CommandParser(path)  # Create the parser, which initializes using data located in the path directory\nsource = CommandSource(callback)  # Create a source, which is used to talk back to the caller\n\ninput_ = 'command-that-returns-wow arg1 arg2'  # this command would call zzz__command-that-returns-wow.py with arg1 and arg2\n\nparser.parse(CommandContext(input_, source))  # Parse the new context and run the command and callback (If no errors occur)\nassert output == 'wow'\n```\n\n### Command metadata:\n\nMetadata for commands are stored in the `commands.json` file of the `CommandParser.commands_path` directory.\nThis is where all the data for the parser is loaded and stored.\n\nAll `commands.json` files are validated with [JSON Schemas][json-schema] through the [jsonschema][PyPIjsonschema] python package\n\n#### commands.json [Draft-07] JSON Schema | [raw][schema-command]\n\n| key             | type                 | description                                                                             | default  | required |\n|-----------------|----------------------|-----------------------------------------------------------------------------------------|----------|----------|\n| `commandPrefix` | _string_             | Strings must start with this prefix, otherwise it is ignored. Empty string accepts all. | **N/A**  | **Yes**  |\n| `commands`      | _array_[**Command**] | Contains metadata for the stored command modules.                                       | **N/A**  | **Yes**  |\n\n#### Command object [Draft-07] JSON Schema | [raw][schema-parser]\n\n| key           | type                 | description                                                                                       | default  | required |\n|---------------|----------------------|---------------------------------------------------------------------------------------------------|----------|----------|\n| `name`        | _string_             | Uniquely identifies the command to the CommandParser.                                             | **N/A**  | **Yes**  |\n| `usage`       | _string_             | Usage information (How to use args).                                                              | \"\"       | No       |\n| `description` | _string_             | Description of command.                                                                           | \"\"       | No       |\n| `permission`  | _integer_            | The permission level the CommandSource requires to run the command.                               | 0        | No       |\n| `function`    | _boolean_, _null_    | Whether there is an associated python module to load.                                             | null     | No       |\n| `children`    | _array_[**Command**] | Sub-commands; these are handled by the parent's function. (No associated modules for themselves). | []       | No       |\n| `overridable` | _boolean_            | Whether the CommandParser can override any data inside this object (must be manually enabled).    | true     | No       |\n| `disabled`    | _boolean_            | If **true** still load command, but raise a DisabledError when attempting to execute.             | false    | No       |\n\n**NOTE:** Commands modules are not loaded unless they are listed in `commands.json` with the `function` key set to _true_.\n\n#### Example `commands.json` contents:\n```json\n{\n  \"commandPrefix\": \"!\",\n  \"commands\": [\n    {\n      \"name\": \"test\",\n      \"usage\": \"test [*args:any]\",\n      \"description\": \"Test command.\",\n      \"permission\": 500,\n      \"function\": true\n    },\n    {\n      \"name\": \"test2\",\n      \"function\": false\n    }\n  ]\n}\n```\n\n### Command modules:\n\nDynamically-loaded commands are denoted by filename with a prefix of \"zzz__\". Inside a command module,\nthere is a function defined as `command`. This function will be mapped to a `Command`'s function attribute\nand stored in memory for execution. The function has access to any args that were parsed, as well as kwargs:\n\n1. '**self**' (`Command`), which houses the metadata for the command that's being executed.\n\n2. '**parser**' (`CommandParser`), which stores the list of registered commands and command data.\n\n3. '**context**' (`CommandContext`), which supplies the `CommandSource` and the original text sent for parsing.\n\n- Any custom kwargs passed to `CommandParser.parse(context: CommandContext, **kwargs)`.\n\nSince commands cannot import their own modules, some are included in globals (`math`, `random`, and `string`).\nOther attributes included in the global scope are: `getitem` (_operator.getitem_), and `ImproperUsageError` (_dyncommands.exceptions.ImproperUsageError_).\n\n#### Example command module:\n```python\ndef command(*args, **kwargs):\n    self, context = kwargs.pop('self'), kwargs.pop('context')\n    source = context.source\n    if len(args) == 2:\n        amount, sides = abs(int(getitem(args, 0))), abs(int(getitem(args, 1)))\n        if amount \u003e 0 and sides \u003e 0:\n            dice_rolls = [f\"{(str(i + 1) + ':') if amount \u003e 1 else ''} {str(random.randint(1, sides))}/{sides}\" for i in range(amount)]\n            source.send_feedback(f\"/me \\U0001f3b2 {source.display_name} rolled {'a die' if amount == 1 else str(amount) + ' dice'} with {sides} side{'' if sides == 1 else 's'}: {', '.join(dice_rolls)} \\U0001f3b2\")\n        else:\n            raise ImproperUsageError(self, context)\n    else:\n        raise ImproperUsageError(self, context)\n```\n\nAt any time, you can call `CommandParser.reload()` to reload all command modules and metadata from disk storage.\n\n#### Example file structure:\n    ../\n    │\n    ├───[commands_path]/\n    │       ├─── commands.json\n    │       ├─── zzz__[command1].py\n    │       ├─── zzz__[command2].py\n    │       └─── zzz__[command3].py\n    │\n\n### Adding/Removing Commands:\n\nTo add commands, you can either manually enter data into a `commands.json` file, or use the\n`CommandParser.add_command(text: str, link: bool = False, **kwargs)` method.\nThe easiest way to use this method is to read the command module as text and pass that to the first argument.\nYou can also store command modules online to allow for remote installation, as setting the **link** parameter to **True**\nwill read **text** as a link, and will get the raw text data from that link. Ex: [gist] and [pastebin].\n\n**NOTE:** When adding a command, metadata for 'name' **must** be filled. This can be done in the form of comments.\n\nRemoving an already added command is relatively easy. Just call `CommandParser.remove_command(name: str)` with the name\nof the command that you want to remove, and it will delete both the metadata and the command module from the disk.\n\nIf you don't want to delete the command when removing, a better alternative is to disable it with\n`CommandParser.set_disabled(name: str, value: bool)`.\n\n#### Example of metadata as in-line comments:\n```python\n# Name: points\n# Usage: points [get (username:string) | set (username:string amount:integer)]\n# Description: Get your current points\n# Permission: 0\n# Children: [{'name': 'get', 'usage': 'get (username:string)', 'permission':0}, {'name': 'set', 'usage': 'set (username:string amount:integer)', 'permission':500}]\ndef command(*args, **kwargs):\n    ...\n```\n\n#### Examples of metadata as kwargs:\n```python\nparser = CommandParser('./')\nwith open('some_metadata.json') as _file:\n    get_ = {'name': 'get', 'usage': 'get (username:string)', 'permission':0}\n    set_ = {'name': 'set', 'usage': 'set (username:string amount:integer)', 'permission':500}\n    children = [get_, set_]\n    parser.add_command(_file.read(), name='my-command', description='Command with child commands.', children=children)\n```\n```python\nparser = CommandParser('./')\nwith open('some_metadata.json') as _file:\n    metadata = json.load(_file)\nparser.add_command('https://gist.github.com/random/892hdh2fh389x0wcmksio7m', link=True, **metadata)\n```\n\n### Permission Levels:\n\nThe dyncommand `CommandParser` natively supports permission level handling, so you don't have to implement a similar\nsystem in every command function.\n\nEach command has the metadata value `permission`,\n(with the exception of the special value `-1`) is the minimum permission level required from the `CommandSource`.\n`-1` represents an \"infinite\" requirement, where no `CommandSource` will be able to execute it while the permission\nsystem is active.\n\nTo disable the permission system, set the `CommandParser`'s `_ignore_permission` attribute to True.\n**NOTE:** since this attribute starts with an \"_\", attempting to change it from inside a command's function will result\nin failed compilation and an Exception.\n\n\n[coverage]: https://codecov.io/gh/Cubicpath/dyncommands \"Codecov results\"\n[Draft-07]: https://tools.ietf.org/html/draft-handrews-json-schema-01 \"Draft-07\"\n[gist]: https://gist.github.com \"gist\"\n[homepage]: https://pypi.org/project/dyncommands/ \"dyncommands PyPI\"\n[json-schema]: https://json-schema.org/ \"json-schema.org\"\n[license]: https://choosealicense.com/licenses/mit \"MIT License\"\n[pastebin]: https://pastebin.com \"pastebin\"\n[PyPIjsonschema]: https://pypi.org/project/jsonschema/ \"jsonschema PyPI\"\n[python]: https://www.python.org \"Python\"\n[RestrictedPython]: https://github.com/zopefoundation/RestrictedPython \"RestrictedPython GitHub\"\n[schema-command]: https://raw.githubusercontent.com/Cubicpath/dyncommands/master/src/dyncommands/schemas/command.schema.json# \"Raw Command Schema\"\n[schema-parser]: https://raw.githubusercontent.com/Cubicpath/dyncommands/master/src/dyncommands/schemas/parser.schema.json# \"Raw commands.json Schema\"\n[unittests]: https://github.com/Cubicpath/dyncommands/actions/workflows/tests.yaml \"Test Results\"\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcubicpath%2Fdyncommands","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fcubicpath%2Fdyncommands","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcubicpath%2Fdyncommands/lists"}