-
Notifications
You must be signed in to change notification settings - Fork 13
Feature/extension scripts #224
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
32 commits
Select commit
Hold shift + click to select a range
d97f620
initial work for extension scripts
timabrmsn 43d6670
handle cmd construction on Windows
timabrmsn d1c93cf
use next(iter(dict)) instead of list(dict.keys)[0]
timabrmsn 35e3374
require --python to be first arg to trigger interpreter
timabrmsn 8a6f9ea
adjust arg swap given we know it's arg[0]
timabrmsn a77a5fb
remove cmd name if there's only one
timabrmsn 718942e
rename --python option to --script, document option and add docstring…
timabrmsn c4c3976
alias sdk_options in extensions.py
timabrmsn b668c4d
style
timabrmsn 1dde0f2
allow single commands to execute when no args/options required
timabrmsn 3a2e5e5
style
timabrmsn 1fd7b95
just print executable path instead of exec-ing directly
timabrmsn 119ae7a
remove unused imports
timabrmsn 71671fb
test installable plugin
timabrmsn 97f258f
delete test plugin folder
timabrmsn 1e8e708
add required lib, style
timabrmsn 14e06f8
style
timabrmsn 23633f3
exit if --python is passed to not allow subcommands
timabrmsn 908d1f0
make extensions a module
timabrmsn 1b7dfaa
add unused import ignore to setup.cfg, document import to prevent fut…
timabrmsn 9e57452
Merge branch 'master' into feature/extension_scripts
timabrmsn abfa3db
Add docs and redefine sdk_options to be regular decorator instead of …
timabrmsn 8651fb7
Merge branch 'master' into feature/extension_scripts
timabrmsn 66c1e4d
style
timabrmsn 3d29f2f
update changelog
timabrmsn 1c42b6d
Add note on extensions to README
timabrmsn b6fae55
style
timabrmsn 4e5047e
Merge branch 'master' into feature/extension_scripts
timabrmsn 0fb2fb3
code42cli > Code42 CLI
timabrmsn 1b74cff
remove ignore since we're using all imports now
timabrmsn ae7eb6c
comma
timabrmsn c538691
make readable w linebreaks
timabrmsn File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,101 @@ | ||
# Write custom extension scripts using the Code42 CLI and py42 | ||
|
||
While the Code42 CLI aims to provide an easy way to automate many common Code42 tasks, there will likely be times when | ||
you need to script something the CLI doesn't have out-of-the-box. | ||
|
||
To accommodate for those scenarios, the Code42 CLI exposes a few helper objects in the `code42cli.extensions` module | ||
that make it easy to write custom scripts with `py42` that use features of the CLI (like profiles) to reduce the amount | ||
of boilerplate needed to be productive. | ||
|
||
## Before you begin | ||
|
||
The Code42 CLI is a python application written using the [click framework](https://click.palletsprojects.com/en/7.x/), | ||
and the exposed extension objects are custom `click` classes. A basic knowledge of how to define `click` commands, | ||
arguments, and options is required. | ||
|
||
### The `sdk_options` decorator | ||
|
||
The most important extension object is the `sdk_options` decorator. When you decorate a command you've defined in your | ||
script with `@sdk_options`, it will automatically add `--profile` and `--debug` options to your command. These work the | ||
same as in the main CLI commands. | ||
|
||
Decorating a command with `@sdk_options` also causes the first argument to your command function to be the `state` | ||
object, which contains the initialized py42 sdk. There's no need to handle user credentials or login, the `sdk_options` | ||
does all that for you using the CLI profiles. | ||
|
||
### The `script` group | ||
|
||
The `script` object exposed in the extensions module is a `click.Group` subclass, which allows you to add multiple | ||
sub-commands and group functionality together. While not explicitly required when writing custom scripts, the `script` | ||
group has logic to help handle and log any uncaught exceptions to the `~/.code42cli/log/code42_errors.log` file. | ||
|
||
If only a single command is added to the `script` group, the group will default to that command, so you don't need to | ||
explicitly provide the sub-command name. | ||
|
||
An example command that just prints the username and ID that the sdk is authenticated with: | ||
|
||
```python | ||
import click | ||
from code42cli.extensions import script, sdk_options | ||
|
||
@click.command() | ||
@sdk_options | ||
def my_command(state): | ||
user = state.sdk.users.get_current() | ||
print(user["username"], user["userId"]) | ||
|
||
if __name__ == "__main__": | ||
script.add_command(my_command) | ||
script() | ||
``` | ||
|
||
## Ensuring your script runs in the Code42 CLI python environment | ||
|
||
The above example works as a standalone script, if it were named `my_script.py` you could execute it by running: | ||
|
||
```bash | ||
python3 my_script.py | ||
``` | ||
|
||
However, if the Code42 CLI is installed in a different python environment than your `python3` command, it might fail to | ||
import the extensions. | ||
|
||
To workaround environment and path issues, the CLI has a `--python` option that prints out the path to the python | ||
executable the CLI uses, so you can execute your script with`$(code42 --python) script.py` on Mac/Linux or | ||
`&$(code42 --python) script.py` on Windows to ensure it always uses the correct python path for the extension script to | ||
work. | ||
|
||
## Installing your extension script as a Code42 CLI plugin | ||
|
||
The above example works as a standalone script, but it's also possible to install that same script as a plugin into the | ||
main CLI itself. | ||
|
||
Assuming the above example code is in a file called `my_script.py`, just add a file `setup.py` in the same directory | ||
with the following: | ||
|
||
```python | ||
from distutils.core import setup | ||
|
||
setup( | ||
name="my_script", | ||
version="0.1", | ||
py_modules=["my_script"], | ||
install_requires=["code42cli"], | ||
entry_points=""" | ||
[code42cli.plugins] | ||
my_command=my_script:my_command | ||
""", | ||
) | ||
``` | ||
|
||
The `entry_points` section tells the Code42 CLI where to look for the commands to add to its main group. If you have | ||
multiple commands defined in your script you can add one per line in the `entry_points` and they'll all get installed | ||
into the Code42 CLI. | ||
|
||
Once your `setup.py` is ready, install it with pip while in the directory of `setup.py`: | ||
|
||
``` | ||
$(code42 --python) -m pip install . | ||
``` | ||
|
||
Then running `code42 -h` should show `my-command` as one of the available commands to run! |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
from code42cli.click_ext.groups import ExtensionGroup | ||
from code42cli.main import CONTEXT_SETTINGS | ||
from code42cli.options import debug_option | ||
from code42cli.options import pass_state | ||
from code42cli.options import profile_option | ||
|
||
|
||
def sdk_options(f): | ||
"""Decorator that adds two `click.option`s (--profile, --debug) to wrapped command, as well as | ||
passing the `code42cli.options.CLIState` object using the [click.make_pass_decorator](https://click.palletsprojects.com/en/7.x/api/#click.make_pass_decorator), | ||
which automatically instantiates the `py42` sdk using the Code42 profile provided from the `--profile` | ||
option. The `py42` sdk can be accessed from the `state.sdk` attribute. | ||
|
||
Example: | ||
|
||
@click.command() | ||
@sdk_options | ||
def get_current_user_command(state): | ||
my_user = state.sdk.users.get_current() | ||
print(my_user) | ||
""" | ||
f = profile_option()(f) | ||
f = debug_option()(f) | ||
f = pass_state(f) | ||
return f | ||
|
||
|
||
script = ExtensionGroup(context_settings=CONTEXT_SETTINGS) | ||
"""A `click.Group` subclass that enables the Code42 CLI's custom error handling/logging to be used | ||
in extension scripts. If only a single command is added to the `script` group it also uses that | ||
command as the default, so the command name doesn't need to be called explicitly. | ||
|
||
Example: | ||
|
||
@click.command() | ||
@click.argument("guid") | ||
@sdk_options | ||
def get_device_info(state, guid) | ||
device = state.sdk.devices.get_by_guid(guid) | ||
print(device) | ||
|
||
if __name__ == "__main__": | ||
script.add_command(my_command) | ||
script() | ||
|
||
The script can then be invoked directly without needing to call the `get-device-info` subcommand: | ||
|
||
python script.py --profile my_profile <guid> | ||
""" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe you just want to be really explicit, but this could just be
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Or even
sdk_options = sdk_opts
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I mainly wanted to have the object we expose be it's own thing in case we ever want to change our internal
sdk_options
we're not having to work around the extension backwards compatibility.