diff --git a/README.md b/README.md index 4cbb877..139dd05 100644 --- a/README.md +++ b/README.md @@ -1,226 +1 @@ -# Transcriptic Runner - -The Transcriptic Runner is a command-line tool for interacting with the -Transcriptic API to submit and analyze protocols as well as upload them as packages to Transcriptic's website. - -For more information on uploading and packaging releases, see the [Transcriptic Developer Hub](http://developers.transcriptic.com/v1.0/docs/package-creation-quickstart#packaging-and-uploading) - - -## Installation - -``` -$ pip install transcriptic -``` - -or - -``` -$ git clone https://github.com/transcriptic/runner.git -$ cd runner -$ pip install . -``` - -to upgrade to the latest version using pip or check whether you're already up to date: -``` -$ pip install transcriptic --upgrade -``` - - -## Usage - -Access help by typing `$ transcriptic --help` or `$ transcriptic [COMMAND] --help` - -## Mandatory first step: -**Log in to your Transcriptic account** -**\*Before using the runner, you'll need to log in to Transcriptic to fetch your -access key information. This will be saved in `~/.transcriptic` for future -commands.**\* - -``` -$ transcriptic login -Email: sanger@transcriptic.com -Password: -Logged in as sanger@transcriptic.com (cambridge) -``` - -## The Basics -**Preview Protocol Output** - -Previewing a protocol supplies a script with parameters supplied in the "preview" section of a `manifest.json` file. Read more about this below. -``` -$ transcriptic preview MyProtocol -``` - -**Analyze a Protocol** - -To check whether your Autoprotocol is valid using Transcriptic's server-side checker, pipe any script that prints Autoprotocol to STDOUT to `transcriptic analyze`: -``` -$ python my_protocol.py | transcriptic analyze -✓ Protocol analyzed - 2 instructions - 1 container -``` -alternatively: -``` -$ transcriptic preview MyProtocol | transcriptic analyze -``` - -**Submit a Protocol to Transcriptic** -``` -$ python my_protocol.py | transcriptic submit --project "sequencing" --title "Sequencing run" -Run created: https://secure.transcriptic.com/cambridge/sequencing/r1xa043277aekj -``` - -**Submit a Protocol to Transcriptic in Test Mode** -``` -$ python my_protocol.py | transcriptic submit --project "sequencing" --title "Sequencing run" --test -``` - -**Translate a Protocol to English** - -Pipe any valid autoprotocol to `transcriptic summarize` to get a summary of each step -``` -$ transcriptic preview MyProtocol | transcriptic summarize -``` - -### Project Management -**List Existing Projects within Your Organization** -``` -$ transcriptic projects -``` - -**Create a New Project** -``` -$ transcriptic new-project "Genotype All The Things" -``` - -### Packaging and Releasing - -**Create a New Empty Package** -``` -$ trancriptic new-package "my_package" "This is a description for my package of protocols" -``` - -**List Existing Package Names and ids** -``` -$ transcriptic packages -``` - -**Ititialize a Directory With an empty manifest template** - -The init command creates an empty `manifest.json` file with the proper structure within the current directory. Read below or [here](https://developers.transcriptic.com/v1.0/docs/the-manifest) to find out more about what a manifest does. Command will prompt to overwrite if your folder already contains a file called `manifest.json`. -``` -$ transcriptic init -``` - -**Compress All Files in Working Directory for Release** -``` -$ transcriptic release -``` -passing a --name argument allows you to name your release, otherwise it will be named `release_` automatically - -**Compress all files in working directory for release and upload to a specific package** -``` -$ transcriptic release my_package -``` - -**Upload an existing compressed release to a package** -``` -$ transcriptic upload release_v1.0.0.zip my_package -``` - - -### Preview a Protocol - -The [autoprotocol-python](https://github.com/autoprotocol/autoprotocol-python) library helps you generate Autoprotocol with easy to use functions. [autoprotocol.harness](https://github.com/autoprotocol/autoprotocol-python/blob/master/autoprotocol/harness.py) parses a set of typed input parameters contained in a `manifest.json` file and passes them back to the specified script when you run `transcriptic preview` (see above). Input types also define protocol browser UI elements on transcriptic's website. - -**Example -The example below assumes the following file structure: -``` -protocols/ - manifest.json - requirements.txt - my_protocols/ - __init__.py - sample_protocol.py -``` - -A manifest.json file contains metadata about protocols required when uploading a package to Transcriptic. A package can contain many protocols but for our example it will contain just one. The `"inputs"` stanza defines expected parameter types which translate into the proper UI elements for that type when you upload the package to Transcriptic. Read more about the manifest file [here](http://developers.transcriptic.com/v1.0/docs/the-manifest). The preview section serves to provide your script with hard-coded parameters and refs for local testing: -```json -{ - "version": "1.0.0", - "format": "python", - "license": "MIT", - "protocols": [ - { - "name": "SampleProtocol", - "command_string": "python -m my_protocols.sample_protocol", - "description": "this is a sample protocol", - "inputs": { - "source_sample": { - "type": "aliquot", - "description": "A sample source aliquot", - }, - "dest_sample": { - "type": "aliquot", - "description": "A sample destination aliquot" - }, - "transfer_vol": { - "type": "volume", - "description": "Volume to transfer", - "default": "12:microliter" - } - }, - "preview": { - "refs": { - "sample_plate": { - "type": "96-pcr", - "discard": true - } - }, - "parameters": { - "source_sample": "sample_plate/A1", - "dest_sample": "sample_plate/A2", - "transfer_vol": "5:microliter" - } - }, - "dependencies": [] - } - ] -} -``` - -The following is what your `sample_protocol.py` file would look like. Note that there is no need to declare a Protocol object within the script or print the protocol to standard out, both of these things are taken care of by `autoprotocol.harness`. **The `protocol_name` parameter in `autoprotocol.harness.run()` must match the name of that protocol within your manifest.json file**: -```python -def sample_protocol(protocol, params): - protocol.transfer(params["source_sample"], - params["dest_sample"], - params["transfer_vol"]) - -if __name__ == "__main__": - from autoprotocol.harness import run - run(sample_protocol, protocol_name="SampleProtocol") -``` - -**Preview a Protocol's Output on the Command Line:** -``` -$ transcriptic preview SampleProtocol -``` - -**Run a protocol and view its output on the command line by passing it an external .json file with parameters and refs (instead of using your `manifest.json`'s "preview" section)**: -``` -$ transcriptic run SampleProtocol protocol_params.json -``` - -To submit the resulting protocol to transcriptic or analyze it, pipe that result to `transcriptic submit` or `transcriptic analyze` as above. -``` -$ transcriptic preview SampleProtocol | transcriptic analyze -``` - -When you're ready to upload a package to Transcriptic, make sure to include the version of autoprotocol and any other packages you might have used in your `requirements.txt` file: -``` -autoprotocol==2.1.0 -``` - -A release consists of everything within the protocols_folder folder **(but do not zip the folder itself: the manifest.json file must be at the top level of the archive.)** - +### This repository is deprecated as of October 26th, 2015. Please refer to [transcriptic/transcriptic](https://github.com/transcriptic/transcriptic) for the most up-to-date code and additional functionality. diff --git a/ap2en.py b/ap2en.py index a305977..82bba2a 100644 --- a/ap2en.py +++ b/ap2en.py @@ -15,12 +15,13 @@ def __init__(self, protocol_obj, parsed_output = None): def parse(self, obj): self.instructions = obj['instructions'] + self.refs = obj['refs'] parsed_output = [] for i in self.instructions: try: - output = eval("self." + i['op'])(i) + output = getattr(self, i['op'])(i) parsed_output.extend(output) if isinstance(output, list) else parsed_output.append(output) - except NameError: + except AttributeError: parsed_output.append("[Unknown instruction]") for i, p in enumerate(parsed_output): print "%d. %s" % (i+1, p) @@ -103,11 +104,18 @@ def spread(self, opts): def stamp(self, opts): stamps = [] - for t in opts['transfers']: - stamps.append("Transfer from %s quadrant %s to %s quadrant %s" % - (self.platename(t['from']), self.well(t['from']), - self.platename(t['to']), self.well(t['to'])) - ) + for g in opts['groups']: + for pip in g: + if pip == "transfer": + stamps.extend(["Stamp %s from source origin %s " + "to destination origin %s %s (%s)" % + (self.unit(p['volume']), + p['from'], + p['to'], + ("with the same set of tips as previous" if (len(g[pip]) > 1 and i > 0) else ""), + ("%s rows x %s columns" % (g['shape']['rows'], g['shape']['columns'])) + ) for i, p in enumerate(g[pip]) + ]) return stamps def thermocycle(self, opts): @@ -129,8 +137,8 @@ def pipette(self, opts): (self.unit(p['volume']), p['from'], p['to'], - ("with one tip" if len(g[pip]) > 1 else "") - ) for p in g[pip] + ("with the same tip as previous" if (len(g[pip]) > 1 and i > 0) else "") + ) for i, p in enumerate(g[pip]) ]) elif pip == "distribute": pipettes.append("Distribute from %s into %s" % diff --git a/screenshots/help.png b/screenshots/help.png new file mode 100644 index 0000000..fc90e05 Binary files /dev/null and b/screenshots/help.png differ diff --git a/screenshots/manifest_json.png b/screenshots/manifest_json.png new file mode 100644 index 0000000..8fe3e60 Binary files /dev/null and b/screenshots/manifest_json.png differ diff --git a/screenshots/packagepublished.png b/screenshots/packagepublished.png new file mode 100644 index 0000000..ef0f944 Binary files /dev/null and b/screenshots/packagepublished.png differ diff --git a/screenshots/packageunpublished.png b/screenshots/packageunpublished.png new file mode 100644 index 0000000..ab16d70 Binary files /dev/null and b/screenshots/packageunpublished.png differ diff --git a/screenshots/projectpage.png b/screenshots/projectpage.png new file mode 100644 index 0000000..3fa1ddd Binary files /dev/null and b/screenshots/projectpage.png differ diff --git a/screenshots/projectpage2.png b/screenshots/projectpage2.png new file mode 100644 index 0000000..2bfe90a Binary files /dev/null and b/screenshots/projectpage2.png differ diff --git a/screenshots/requirements_txt.png b/screenshots/requirements_txt.png new file mode 100644 index 0000000..6434d86 Binary files /dev/null and b/screenshots/requirements_txt.png differ diff --git a/screenshots/test_py.png b/screenshots/test_py.png new file mode 100644 index 0000000..a941326 Binary files /dev/null and b/screenshots/test_py.png differ diff --git a/screenshots/transcripticanalyze.png b/screenshots/transcripticanalyze.png new file mode 100644 index 0000000..5e9db1e Binary files /dev/null and b/screenshots/transcripticanalyze.png differ diff --git a/screenshots/transcripticdelete-package.png b/screenshots/transcripticdelete-package.png new file mode 100644 index 0000000..bdd448c Binary files /dev/null and b/screenshots/transcripticdelete-package.png differ diff --git a/screenshots/transcripticdelete-project-with-run.png b/screenshots/transcripticdelete-project-with-run.png new file mode 100644 index 0000000..1fcb37b Binary files /dev/null and b/screenshots/transcripticdelete-project-with-run.png differ diff --git a/screenshots/transcripticdelete-project.png b/screenshots/transcripticdelete-project.png new file mode 100644 index 0000000..f80791a Binary files /dev/null and b/screenshots/transcripticdelete-project.png differ diff --git a/screenshots/transcripticinit.png b/screenshots/transcripticinit.png new file mode 100644 index 0000000..be2258a Binary files /dev/null and b/screenshots/transcripticinit.png differ diff --git a/screenshots/transcripticlogin.png b/screenshots/transcripticlogin.png new file mode 100644 index 0000000..bb89013 Binary files /dev/null and b/screenshots/transcripticlogin.png differ diff --git a/screenshots/transcripticnew-package.png b/screenshots/transcripticnew-package.png new file mode 100644 index 0000000..c1a9863 Binary files /dev/null and b/screenshots/transcripticnew-package.png differ diff --git a/screenshots/transcripticnew-project.png b/screenshots/transcripticnew-project.png new file mode 100644 index 0000000..af35ce2 Binary files /dev/null and b/screenshots/transcripticnew-project.png differ diff --git a/screenshots/transcripticpackages.png b/screenshots/transcripticpackages.png new file mode 100644 index 0000000..8608155 Binary files /dev/null and b/screenshots/transcripticpackages.png differ diff --git a/screenshots/transcripticpreview.png b/screenshots/transcripticpreview.png new file mode 100644 index 0000000..9d83e10 Binary files /dev/null and b/screenshots/transcripticpreview.png differ diff --git a/screenshots/transcripticprojects.png b/screenshots/transcripticprojects.png new file mode 100644 index 0000000..b7d2d3e Binary files /dev/null and b/screenshots/transcripticprojects.png differ diff --git a/screenshots/transcripticrelease.png b/screenshots/transcripticrelease.png new file mode 100644 index 0000000..3e13db4 Binary files /dev/null and b/screenshots/transcripticrelease.png differ diff --git a/screenshots/transcripticrelease2.png b/screenshots/transcripticrelease2.png new file mode 100644 index 0000000..40d449e Binary files /dev/null and b/screenshots/transcripticrelease2.png differ diff --git a/screenshots/transcripticreleaseonly.png b/screenshots/transcripticreleaseonly.png new file mode 100644 index 0000000..a32be2b Binary files /dev/null and b/screenshots/transcripticreleaseonly.png differ diff --git a/screenshots/transcripticsubmit.png b/screenshots/transcripticsubmit.png new file mode 100644 index 0000000..aaebc39 Binary files /dev/null and b/screenshots/transcripticsubmit.png differ diff --git a/screenshots/transcripticsummarize.png b/screenshots/transcripticsummarize.png new file mode 100644 index 0000000..f24f25e Binary files /dev/null and b/screenshots/transcripticsummarize.png differ diff --git a/screenshots/transcripticupload.jpg b/screenshots/transcripticupload.jpg new file mode 100644 index 0000000..a0e407f Binary files /dev/null and b/screenshots/transcripticupload.jpg differ diff --git a/setup.py b/setup.py index 29aaa5d..77625ff 100644 --- a/setup.py +++ b/setup.py @@ -2,11 +2,12 @@ setup( name='transcriptic', - version='1.3.14', + version='1.4.3', py_modules=['transcriptic', 'ap2en'], install_requires=[ 'Click>=5.1', - 'requests' + 'requests', + 'autoprotocol' ], entry_points=''' [console_scripts] diff --git a/transcriptic.py b/transcriptic.py index 5942f5e..6698e59 100644 --- a/transcriptic.py +++ b/transcriptic.py @@ -27,6 +27,13 @@ def __init__(self, api_root, email, token, organization): self.email = email self.token = token self.organization = organization + self.default_headers = { + 'X-User-Email': self.email, + 'X-User-Token': self.token, + 'Content-Type': 'application/json', + 'Accept': 'application/json', + } + @staticmethod def from_file(path): @@ -34,6 +41,7 @@ def from_file(path): cfg = json.loads(f.read()) return Config(**cfg) + def save(self, path): with click.open_file(expanduser(path), 'w') as f: f.write(json.dumps({ @@ -43,31 +51,34 @@ def save(self, path): 'api_root': self.api_root, }, indent=2)) + def url(self, path): return "%s/%s/%s" % (self.api_root, self.organization, path) + def post(self, path, **kwargs): - default_headers = { - 'X-User-Email': self.email, - 'X-User-Token': self.token, - 'Content-Type': 'application/json', - 'Accept': 'application/json', - } + default_headers = self.default_headers default_headers.update(kwargs.pop('headers', {})) return requests.post(self.url(path), headers=default_headers, **kwargs) + def put(self, path, **kwargs): + default_headers = self.default_headers + default_headers.update(kwargs.pop('headers', {})) + return requests.put(self.url(path), headers=default_headers, **kwargs) + def get(self, path, **kwargs): - default_headers = { - 'X-User-Email': self.email, - 'X-User-Token': self.token, - 'Content-Type': 'application/json', - 'Accept': 'application/json', - } + default_headers = self.default_headers default_headers.update(kwargs.pop('headers', {})) return requests.get(self.url(path), headers=default_headers, **kwargs) + def delete(self, path, **kwargs): + default_headers = self.default_headers + default_headers.update(kwargs.pop('headers', {})) + return requests.delete(self.url(path), headers=default_headers, **kwargs) + + @click.group() @click.option('--apiroot', default=None) @click.option('--config', @@ -90,6 +101,7 @@ def cli(ctx, apiroot, config, organization): "`transcriptic login` ...") ctx.invoke(login) + @cli.command() @click.argument('file', default='-') @click.option('--project', '-p', @@ -138,7 +150,7 @@ def submit(ctx, file, project, title, test): @cli.command() -@click.argument('package', required=False) +@click.argument('package', required=False, metavar="PACKAGE") @click.option('--name', '-n', help="Optional name for your zip file") @click.pass_context def release(ctx, name=None, package=None): @@ -174,8 +186,9 @@ def makezip(d, archive): @cli.command("upload") -@click.argument('archive', required=True, type=click.Path(exists=True)) -@click.argument('package', required=True) +@click.argument('archive', required=True, type=click.Path(exists=True), + metavar="ARCHIVE") +@click.argument('package', required=True, metavar="PACKAGE") @click.pass_context def upl(ctx, archive, package): """Upload an existing archive to an existing package""" @@ -247,16 +260,45 @@ def upl(ctx, archive, package): bar.update(30) if errors: click.echo("\nPackage upload to %s unsuccessful. " - "The following error was " - "returned: %s" % + "The following error(s) was " + "returned: \n%s" % (get_package_name(package_id), - (',').join(e.get('message', '[Unknown]') for + ('\n').join(e.get('message', '[Unknown]') for e in errors))) else: click.echo("\nPackage uploaded successfully! \n" "Visit %s to publish." % ctx.obj.url('packages/%s' % package_id)) +@cli.command() +def protocols(): + '''List protocols within your manifest''' + try: + with click.open_file('manifest.json', 'r') as f: + try: + manifest = json.loads(f.read()) + except ValueError: + click.echo("Error: Your manifest.json file is improperly formatted. " + "Please double check your brackets and commas!") + return + if 'protocols' not in manifest.keys() or not manifest['protocols']: + click.echo("Your manifest.json file doesn't contain any protocols or" + " is improperly formatted.") + return + else: + click.echo('\n{:^60}'.format("Protocols within this manifest:")) + click.echo('{:-^60}'.format('')) + [click.echo("%s%s\n%s" % (p['name'], + (" (" + p.get('display_name') + ")") + if p.get('display_name') else "", + ('{:-^60}'.format("")))) + for p in manifest["protocols"]] + + except IOError: + click.echo("The current directory does not contain a manifest.json file.") + return + + @cli.command() @click.pass_context @click.option("-i") @@ -268,26 +310,37 @@ def packages(ctx, i): if response.status_code == 200: for pack in response.json(): - if pack.get('owner'): - if pack['owner']['email'] == ctx.obj.email: - package_names['yours'][str(pack['name']).lower().replace("com.%s." % ctx.obj.organization, "")] = str(pack['id']) + n = str(pack['name']).lower().replace("com.%s." % ctx.obj.organization, "") + latest = str(pack['latest_version']) if pack['latest_version'] else "-" + if pack.get('owner') and pack['owner']['email'] == ctx.obj.email: + package_names['yours'][n] = {} + package_names['yours'][n]['id'] = str(pack['id']) + package_names['yours'][n]['latest'] = latest else: - package_names['theirs'][str(pack['name']).lower().replace("com.%s." % ctx.obj.organization, "")] = str(pack['id']) + package_names['theirs'][n] = {} + package_names['theirs'][n]['id'] = str(pack['id']) + package_names['theirs'][n]['latest'] = latest if i: return dict(package_names['yours'].items() + package_names['theirs'].items()) else: for category, packages in package_names.items(): if category == "yours": - click.echo('\n{:^80}'.format("YOUR PACKAGES:")) - else: - click.echo('\n{:^80}'.format("OTHER PACKAGES IN YOUR ORG:")) - click.echo('{:^40}'.format("PACKAGE NAME") + "|" + - '{:^40}'.format("PACKAGE ID")) - click.echo('{:-^80}'.format('')) - for name, id in packages.items(): - click.echo('{:<40}'.format(name) + "|" + - '{:^40}'.format(id)) - click.echo('{:-^80}'.format('')) + click.echo('\n{:^90}'.format("YOUR PACKAGES:\n")) + click.echo('{:^30}'.format("PACKAGE NAME") + "|" + + '{:^30}'.format("PACKAGE ID") + + "|" + '{:^30}'.format("LATEST PUBLISHED VERSION")) + click.echo('{:-^90}'.format('')) + elif category == "theirs" and packages.values(): + click.echo('\n{:^90}'.format("OTHER PACKAGES IN YOUR ORG:\n")) + click.echo('{:^30}'.format("PACKAGE NAME") + "|" + + '{:^30}'.format("PACKAGE ID") + "|" + + '{:^30}'.format("LATEST PUBLISHED VERSION")) + click.echo('{:-^90}'.format('')) + for name, p in packages.items(): + click.echo('{:<30}'.format(name) + "|" + + '{:^30}'.format(p['id']) + "|" + + '{:^30}'.format(p['latest'])) + click.echo('{:-^90}'.format('')) @cli.command("new-package") @click.argument('name') @@ -313,6 +366,21 @@ def new_package(ctx, description, name): else: click.echo("There was an error creating this package.") +@cli.command("delete-package") +@click.argument('name') +@click.option('--force', '-f', help="force delete a package without being prompted if you're sure", is_flag=True) +@click.pass_context +def delete_package(ctx, name, force): + '''Delete an existing protocol package''' + id = get_package_id(name) + if id: + if not force: + click.confirm("Are you sure you want to permanently delete the package" + " '%s'? All releases within will be lost." % + get_package_name(id), default=False, abort=True) + click.confirm("Are you really really sure?", default=True) + del_pack = ctx.obj.delete('/packages/%s' % id) + click.echo("Package deleted.") @cli.command() @click.pass_context @@ -321,28 +389,42 @@ def projects(ctx, i): '''List the projects in your organization''' response = ctx.obj.get('') proj_names = {} + proj_cats = {"reg": {}, "pilot": {}} if response.status_code == 200: for proj in response.json()['projects']: proj_names[proj['name']] = proj['id'] + if proj.get("is_developer"): + proj_cats["pilot"][proj['name']] = proj['id'] + else: + proj_cats["reg"][proj['name']] = proj['id'] if i: return {k.lower(): v for k,v in proj_names.items()} else: - click.echo('{:^35}'.format("PROJECT NAME") + "|" + - '{:^35}'.format("PROJECT ID")) - click.echo('{:-^70}'.format('')) - for name, i in proj_names.items(): - click.echo('{:<35}'.format(name) + "|" + - '{:^35}'.format(i)) - click.echo('{:-^70}'.format('')) + for cat, packages in proj_cats.items(): + if cat == "reg": + click.echo('\n{:^80}'.format("PROJECTS:\n")) + click.echo('{:^40}'.format("PROJECT NAME") + "|" + + '{:^40}'.format("PROJECT ID")) + click.echo('{:-^80}'.format('')) + elif cat == "pilot" and packages.values(): + click.echo('\n{:^80}'.format("PILOT PROJECTS:\n")) + click.echo('{:^40}'.format("PROJECT NAME") + "|" + + '{:^40}'.format("PROJECT ID")) + click.echo('{:-^80}'.format('')) + for name, i in packages.items(): + click.echo('{:<40}'.format(name) + "|" + + '{:^40}'.format(i)) + click.echo('{:-^80}'.format('')) else: click.echo("There was an error listing the projects in your " "organization. Make sure your login details are correct.") @cli.command("new-project") -@click.argument('name') +@click.argument('name', metavar="PROJECT_NAME") +@click.option('--dev', '-d', '-pilot', help="Create a pilot project", is_flag=True) @click.pass_context -def new_project(ctx, name): +def new_project(ctx, name, dev): '''Create a new empty project''' existing = ctx.obj.get('') for p in existing.json()['projects']: @@ -350,19 +432,41 @@ def new_project(ctx, name): click.echo("You already have an existing project with the name \"%s\"." " Please choose a different project name." % name) return - new_proj = ctx.obj.post('', - data= json.dumps({ - "name": name - }) - ) + proj_data = {"name": name} + if dev: + proj_data["is_developer"] = True + new_proj = ctx.obj.post('', data= json.dumps(proj_data)) if new_proj.status_code == 201: - click.echo("New project '%s' created with id %s \nView it at %s" % - (name, new_proj.json()['id'], - ctx.obj.url('projects/%s' % new_proj.json()['id']))) + click.echo("New%s project '%s' created with id %s \nView it at %s" % + (" pilot" if dev else "", name, new_proj.json()['id'], + ctx.obj.url('%s' % (new_proj.json()['id'])))) else: click.echo("There was an error creating this package.") +@cli.command("delete-project") +@click.argument('name', metavar="PROJECT_NAME") +@click.option('--force', '-f', help="force delete a project without being prompted if you're sure", is_flag=True) +@click.pass_context +def delete_project(ctx, name, force): + '''Delete an existing project''' + id = get_project_id(name) + if id: + if not force: + click.confirm("Are you sure you want to permanently delete '%s'?" % get_project_name(id), + default=False, + abort=True) + dele = ctx.obj.delete('%s' % id, data=json.dumps({"id": id})) + if dele.status_code == 200: + click.echo("Project deleted.") + else: + click.confirm("You cannot delete a project that contains runs. Archive it instead?", + default=False, abort=True) + arch = ctx.obj.put('%s' % id, data=json.dumps({"project": {"archived": True}})) + if arch.status_code == 200: + click.echo("Project archived.") + + @cli.command() def init(): '''Initialize directory with blank manifest.json file''' @@ -405,8 +509,12 @@ def analyze(ctx, file, test): try: protocol = json.loads(f.read()) except ValueError: - click.echo("Error: Could not analyze since your manifest.json file is " - "improperly formatted.") + click.echo("Error: The Autoprotocol you're trying to analyze is not " + "properly formatted. " + "\nCheck that your manifest.json file is " + "valid JSON \nand/or your script " + "doesn't print anything other than pure Autoprotocol " + "to standard out.") return response = \ ctx.obj.post( @@ -417,7 +525,10 @@ def analyze(ctx, file, test): click.echo(u"\u2713 Protocol analyzed") price(response.json()) elif response.status_code == 422: - click.echo("Error in protocol: %s" % response.text) + click.echo("Error%s in protocol:\n%s" % + (("s" if len(response.json()['protocol']) > 1 else ""), + "".join(["- " + e['message'] + "\n" for + e in response.json()['protocol']]))) else: click.echo("Unknown error: %s" % response.text) @@ -439,7 +550,7 @@ def count(thing, things, num): click.echo("WARNING (%s): %s" % (context, message)) @cli.command() -@click.argument('protocol_name') +@click.argument('protocol_name', metavar="PROTOCOL_NAME") def preview(protocol_name): '''Preview the Autoprotocol output of a script''' with click.open_file('manifest.json', 'r') as f: @@ -480,6 +591,7 @@ def preview(protocol_name): @click.argument('file', default='-') @click.pass_context def summarize(ctx, file): + """Summarize Autoprotocol as a list of plain English steps (WIP)""" with click.open_file(file, 'r') as f: try: protocol = json.loads(f.read()) @@ -490,7 +602,7 @@ def summarize(ctx, file): @cli.command() -@click.argument('protocol_name') +@click.argument('protocol_name', metavar="PROTOCOL_NAME") @click.argument('args', nargs=-1) def run(protocol_name, args): '''Run a protocol by passing it a config file (without submitting or analyzing)''' @@ -583,20 +695,33 @@ def get_project_id(ctx, name): if not id: id = name if name in projs.values() else None if not id: - click.echo("A project with the name or id '%s' was not found in your " + click.echo("A project with the name '%s' was not found in your " "organization." % name) return return id +@click.pass_context +def get_project_name(ctx, id): + projs = {v:k for k,v in ctx.invoke(projects, i=True).items()} + name = projs.get(id) + if not name: + name = id if name in projs.keys() else None + if not name: + click.echo("A project with the id '%s' was not found in your " + "organization." % name) + return + return name + + @click.pass_context def get_package_id(ctx, name): package_names = ctx.invoke(packages, i=True) - package_names = {k.lower(): v for k,v in package_names.items()} + package_names = {k.lower(): v['id'] for k,v in package_names.items()} package_id = package_names.get(name) if not package_id: package_id = name if name in package_names.values() else None - if not package_id and __name__ == "__main__": + if not package_id: click.echo("The package %s does not exist in your organization." % name) return return package_id @@ -604,11 +729,11 @@ def get_package_id(ctx, name): @click.pass_context def get_package_name(ctx, id): - package_names = {v: k for k, v in ctx.invoke(packages, i=True).items()} + package_names = {v['id']: k for k, v in ctx.invoke(packages, i=True).items()} package_name = package_names.get(id) if not package_name: package_name = id if id in package_names.values() else None - if not package_name and __name__ == "__main__": + if not package_name: click.echo("The id %s does not match any package in your organization." % id) return