diff --git a/.gitignore b/.gitignore index 5df9179..891d01c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ build/ -dist/ \ No newline at end of file +dist/ +*.pyc diff --git a/README.md b/README.md index 3ac8710..fee9365 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,12 @@ Feel free to submit a pull request which fixes this. pip install apiary2postman +### Or, run from your checkout + + git clone + cd apiary2postman/apiary2postman + ./apiary2postman.py + # Usage apiary2postman json blueprint.json --output postman.json @@ -47,13 +53,25 @@ Feel free to submit a pull request which fixes this. cat some.blueprint | apiary2postman blueprint > postman.dump -##### To generate a total Postman environment dump from Apiary API, use the `api` subcommand: +##### To generate a total Postman environment dump from Apiary API, use the `api` subcommand with your Apiary API name: - apiary2postman api my_api > postman.dump + apiary2postman api my_api > my_api.dump + +##### To ignore certain Apiary API resource groups, use `--exclude` (case-insensitive substring match): + + apiary2postman --exclude 'IGNORE ME' --exclude 'ME TOO' api my_api > my_api.dump + +##### By default, `apiary2postman` creates one collection per resource group. `--one-collection` allows you to specify the name of a container collection. Folders are created w/in this collection using the resource group names, and requests are added to the folders. Caveat: resources - that would otherwise determine the folder names - are ignored. + + apiary2postman --combine 'My Combined API' api my_api > my_api.dump + +##### If you don't have an API key, log in to [your Apiary account](https://apiary.io), go to Settings, scroll down to Tokens. Generate one if needed, and set the environment variable `APIARY_API_KEY` to that hex string. + + APIARY_API_KEY=ffffffffffffffffffffffffffffffff apiary2postman api my_api > my_api.dump ###### Or to generate only a Postman collection from Apiary API: - apiary2postman --only-collection api my_api > postman.collection + apiary2postman --only-collection api my_api > my_api.collection It's also possible to specify the output file using the `--output`. diff --git a/apiary2postman/apiary2postman.py b/apiary2postman/apiary2postman.py index b3fd48e..efd9362 100755 --- a/apiary2postman/apiary2postman.py +++ b/apiary2postman/apiary2postman.py @@ -1,10 +1,11 @@ #!/usr/bin/env python from sys import stdin, stderr, stdout, argv, exit import argparse +import json import subprocess import os import platform -from converter import write +import converter from blueprint import blueprint2json,fetch_blueprint class bcolors: @@ -77,6 +78,10 @@ def main(): help='the name of the api on apiary. I.e. testapi311 for http://docs.testapi311.apiary.io/') parser_json.add_argument('input', metavar='input', type=file, nargs='?', default=stdin, help='input file, formatted as JSON. If not supplied, stdin is used.') + parser.add_argument('--combine', dest='combine', + help='combine all collections into a a single top-level collection') + parser.add_argument('--exclude', dest='exclude', nargs='+', + help='exclude collections containing the provided (case-insensitive) substrings') parser_blueprint.add_argument('blueprint_input', metavar='input', type=file, nargs='?', default=stdin, help='input file, formatted as Blueprint API Markup. If not supplied, stdin is used.') parser.add_argument('--output', metavar='output', type=argparse.FileType('w'), nargs=1, default=stdout, @@ -108,12 +113,31 @@ def main(): blueprint = fetch_blueprint(args.name[0], apikey) input = blueprint2json(blueprint) + output = args.output if args.output != stdout: output = output[0] - write(input, output, args.only_collection, args.pretty) + # unmarshal from json string + json_obj = json.loads(input) + + if args.only_collection: + # return only the first collection + json_obj = converter.first_collection(json_obj) + else: + json_obj = converter.full_response(json_obj) + + # exclude collections by name + if args.exclude != None and len(args.exclude) > 0: + converter.filter_collections(json_obj, args.exclude) + + # combine all collections into a top-level one, removing existing folders + if args.combine != '': + converter.combine_collections(json_obj, args.combine) + + # write json object out to configured destination, perhaps with whitespace + converter.write(json_obj, out=output, pretty=args.pretty) if __name__ =='__main__': main() diff --git a/apiary2postman/blueprint.pyc b/apiary2postman/blueprint.pyc deleted file mode 100644 index d848ee0..0000000 Binary files a/apiary2postman/blueprint.pyc and /dev/null differ diff --git a/apiary2postman/converter.py b/apiary2postman/converter.py index 1b5a331..9b596ff 100644 --- a/apiary2postman/converter.py +++ b/apiary2postman/converter.py @@ -1,21 +1,25 @@ import json -from sys import stdout +from sys import stdout, stderr from uuid import uuid4 from time import time +import urllib -def _buildCollectionResponse(apiary): - environment = createEnvironment(apiary) +from urimagic import URITemplate + + +def first_collection(json_obj): + environment = createEnvironment(json_obj) # Create the collection collections = parseResourceGroups( - apiary['resourceGroups'], + json_obj['resourceGroups'], environment['values'], True) result = { 'id' : str(uuid4()), - 'name' : apiary['name'], - 'description' : apiary['description'], + 'name' : json_obj['name'], + 'description' : json_obj['description'], 'timestamp' : int(time()), 'remote_id' : 0, 'synced' : False, @@ -31,9 +35,9 @@ def _buildCollectionResponse(apiary): return result -def _buildFullResponse(apiary): +def full_response(json_obj): # Create the Environment - environment = createEnvironment(apiary) + environment = createEnvironment(json_obj) # Create the Header result = { @@ -45,24 +49,76 @@ def _buildFullResponse(apiary): # Create the collection result['collections'] = parseResourceGroups( - apiary['resourceGroups'], + json_obj['resourceGroups'], result['environments'][0]['values'], False) return result -def write(json_data, out=stdout, only_collection=False, pretty=False): - json_obj = json.loads(json_data) - - if only_collection: - result_out = _buildCollectionResponse(json_obj) - else: - result_out = _buildFullResponse(json_obj) +def filter_collections(obj, exclude): + new = [] + exclude = [x.lower() for x in exclude] + for key in obj.iterkeys(): + if key == 'collections': + for coll in obj[key]: + append = True + for ex in exclude: + if ex in coll['name'].lower(): + append = False + if append: + new.append(coll) + obj[key] = new + break + +def combine_collections(obj, name): + # One top-level collection + # Each previously top-level collection becomes a folder + # The folders are discarded, after linking their requests into the new folder + for key in obj.iterkeys(): + if key == 'collections': + obj[key] = [_reorgCollections(obj[key], name)] + +def _folderFromCollection(c): + return { + 'name': c['name'], + 'id': str(uuid4()), + 'description': c['description'], + 'order': [], + 'collection_id': c['id'], + 'collection_name': c['name'], + } +def _reorgCollections(colls, name): + top = { + 'name': name, + 'id': str(uuid4()), + 'folders': [], + 'requests': [], + 'description': '', + 'timestamp': 0, + 'remote_id': 0, + 'order': [], + 'synced': False, + } + for c in colls: + f = _folderFromCollection(c) + f['collection_id'] = top['id'] + f['collection_name'] = top['name'] + top['folders'].append(f) + top['order'].append(f['id']) + for r in c['requests']: + # add requests to folder where r['collection_id'] == c['id'] + r['collectionId'] = top['id'] + r['folder'] = f['id'] + f['order'].append(r['id']) + top['requests'].append(r) + return top + +def write(json_data, out=stdout, pretty=False): if pretty: - json.dump(result_out, out, indent=2, separators=(',', ': ')) + json.dump(json_data, out, indent=2, separators=(',', ': ')) else: - json.dump(result_out, out) + json.dump(json_data, out) def createEnvironment(json_obj): @@ -111,7 +167,7 @@ def parseResourceGroups(resourceGroups, environment_vals, only_collection): folder['collection_id'] = collection['id'] folder['collection_name'] = collection['name'] - sub_url = resource['uriTemplate'] + sub_url = URITemplate(resource['uriTemplate']) for action in resource['actions']: request = dict() request['id'] = str(uuid4()) @@ -122,11 +178,13 @@ def parseResourceGroups(resourceGroups, environment_vals, only_collection): request['descriptionFormat'] = 'html' request['method'] = action['method'] - request['url'] = "{{HOST}}"+sub_url + params = {p['name']: p['example'] for p in action['parameters']} + sub_url_str = urllib.unquote(sub_url.expand(**params).string).encode('utf8') + request['url'] = "{{HOST}}"+sub_url_str if only_collection: for value in environment_vals: if value['name'] == 'HOST': - request['url'] = value['value'] + sub_url + request['url'] = value['value'] + sub_url_str request['dataMode'] = 'params' request['data'] = [] @@ -138,29 +196,24 @@ def parseResourceGroups(resourceGroups, environment_vals, only_collection): request['responses'] = [] request['synced'] = False - for parameter in resource['parameters']: - request['url'] = request['url'].replace('{'+ parameter['name'] +'}', parameter['example']) - - headers = [] + headers = {} for example in action['examples']: # Add Headers for request_ex in example['requests']: - for header in request_ex['headers']: - headers.append(header['name'] + ": " + header['value']) + headers.update({h['name']: h['value'] for h in request_ex['headers']}) if len(request_ex['body']) > 0: request['dataMode'] = 'raw' request['data'] = request_ex['body'] # Add Accept header to request based on response model (hack?) + # EQD: This is not strictly correct since only 1 Accept header will appear in headers for response in example['responses']: - for header in response['headers']: - if header['name'] != 'Content-Type': - continue - if 'Accept: ' + header['value'] not in headers: - headers.append('Accept: ' + header['value']) - request['headers'] = '\n'.join(headers) + content_types = [r['value'] for r in response['headers'] if r['name'].lower() == 'content-type'] + if len(content_types) > 0 and 'Accept' not in headers: + headers['Accept'] = content_types[0] + request['headers'] = '\n'.join(['%s: %s' % (k, v) for k,v in headers.iteritems()]) # Add reference to collection to this request # The collectionId field refers to the parent collection, not the folder request['collectionId'] = collection['id'] diff --git a/apiary2postman/converter.pyc b/apiary2postman/converter.pyc deleted file mode 100644 index 38ff518..0000000 Binary files a/apiary2postman/converter.pyc and /dev/null differ diff --git a/setup.py b/setup.py index 2a2e38e..82c5f14 100644 --- a/setup.py +++ b/setup.py @@ -10,6 +10,7 @@ download_url = 'https://github.com/thecopy/apiary2postman/tarball/0.4.8', keywords = ['apiary', 'blueman', 'postman'], # arbitrary keywords classifiers = [], + install_requires = ['urimagic'], entry_points={ 'console_scripts': [ 'apiary2postman = apiary2postman.apiary2postman:main',