GithubHelp home page GithubHelp logo

martyzz1 / heroku3.py Goto Github PK

View Code? Open in Web Editor NEW
118.0 118.0 74.0 414 KB

This is the updated Python wrapper for the Heroku API V3. https://devcenter.heroku.com/articles/platform-api-reference The Heroku REST API allows Heroku users to manage their accounts, applications, addons, and other aspects related to Heroku. It allows you to easily utilize the Heroku platform from your applications.

License: Other

Python 100.00%

heroku3.py's Introduction

Heroku3.py

image

image

image

image

This is the updated Python wrapper for the Heroku API V3. The Heroku REST API allows Heroku users to manage their accounts, applications, addons, and other aspects related to Heroku. It allows you to easily utilize the Heroku platform from your applications.

Introduction

First instantiate a heroku_conn as above:

import heroku3
heroku_conn = heroku3.from_key('YOUR_API_KEY')

Interact with your applications:

>>> heroku_conn.apps()
[<app 'sharp-night-7758'>, <app 'empty-spring-4049'>, ...]

>>> app = heroku_conn.apps()['sharp-night-7758']

General notes on Debugging

Heroku provides some useful debugging information. This code exposes the following

Ratelimit Remaining

Get the current ratelimit remaining:

num = heroku_conn.ratelimit_remaining()

Last Request Id

Get the unique ID of the last request sent to heroku to give them for debugging:

id = heroku_conn.last_request_id

General notes about list Objects

The new heroku3 API gives greater control over the interaction of the returned data. Primarily this centres around calls to the api which result in list objects being returned. e.g. multiple objects like apps, addons, releases etc.

Throughout the docs you'll see references to using limit & order_by. Wherever you see these, you should be able to use limit, order_by, sort and valrange.

You can control ordering, limits and pagination by supplying the following keywords:

order_by=<'id'|'version'>
limit=<num>
valrange=<string> - See api docs for this, This value is passed straight through to the API call *as is*.
sort=<'asc'|'desc'>

You'll have to investigate the api for each object's *Accept-Ranges* header to work out which fields can be ordered by

Examples

List all apps in name order:

heroku_conn.apps(order_by='name')

List the last 10 releases:

app.releases(order_by='version', limit=10, sort='desc')
heroku_conn.apps()['empty-spring-4049'].releases(order_by='version', limit=10, sort='desc')

List objects can be referred to directly by any of their primary keys too:

app = heroku_conn.apps()['myapp']
dyno = heroku_conn.apps()['myapp_id'].dynos()['web.1']
proc = heroku_conn.apps()['my_app'].process_formation()['web']

Be careful if you use *limit* in a list call *and* refer directly to an primary key E.g.Probably stupid...:

dyno = heroku_conn.apps()['myapp'].dynos(limit=1)['web.1']

General Notes on Objects

To find out the Attributes available for a given object, look at the corresponding Documentation for that object. e.g.

Formation Object:

>>>print(feature.command)
bundle exec rails server -p $PORT

>>>print(feature.created_at)
2012-01-01T12:00:00Z

>>>print(feature.id)
01234567-89ab-cdef-0123-456789abcdef

>>>print(feature.quantity)
1
>>>print(feature.size)
1
>>>print(feature.type)
web

>>>print(feature.updated_at)
2012-01-01T12:00:00Z

Switching Accounts Mid Flow

It is also possible to change the underlying heroku_connection at any point on any object or listobject by creating a new heroku_conn and calling change_connection:

heroku_conn1 = heroku3.from_key('YOUR_API_KEY')
heroku_conn2 = heroku3.from_key('ANOTHER_API_KEY')
app = heroku_conn1.apps()['MYAPP']
app.change_connection(heroku_conn2)
app.config() # this call will use heroku_conn2
## or on list objects
apps = heroku_conn1.apps()
apps.change_connection(heroku_conn2)
for app in apps:
    config = app.config()

Legacy API Calls

The API has been built with an internal legacy=True ability, so any functionlity not implemented in the new API can be called via the previous legacy API. This is currently only used for rollbacks.

Object API

Account

Get account:

account = heroku_conn.account()

Change Password:

account.change_password("<current_password>", "<new_password>")

SSH Keys

List all configured keys:

keylist = account.keys(order_by='id')

Add Key:

account.add_key(<public_key_string>)

Remove key:

account.remove_key(<public_key_string - or fingerprint>)

Account Features (Heroku Labs)

List all configured account "features":

featurelist = account.features()

Disable a feature:

feature = account.disable_feature(id_or_name)
feature.disable()

Enable a feature:

feature = account.enable_feature(id_or_name)
feature.enable()

Plans - or Addon Services

List all available Addon Services:

addonlist = heroku_conn.addon_services(order_by='id')
addonlist = heroku_conn.addon_services()

Get specific available Addon Service:

addonservice = heroku_conn.addon_services(<id_or_name>)

App

The App Class is the starting point for most of the api functionlity.

List all apps:

applist = heroku_conn.apps(order_by='id')
applist = heroku_conn.apps()

Get specific app:

app = heroku_conn.app(<id_or_name>)
app = heroku_conn.apps()[id_or_name]

Create an app:

app = heroku_conn.create_app(name=None, stack_id_or_name='cedar', region_id_or_name=<region_id>)

Destroy an app (Warning this is irreversible):

app.delete()

Addons

List all Addons:

addonlist = app.addons(order_by='id')
addonlist = applist[<id_or_name>].addons(limit=10)
addonlist = heroku_conn.addons(<app_id_or_name>)

Install an Addon:

addon = app.install_addon(plan_id_or_name='<id>', config={})
addon = app.install_addon(plan_id_or_name='<name>', config={})
addon = app.install_addon(plan_id_or_name=addonservice.id, config={})
addon = app.install_addon(plan_id_or_name=addonservice.id, config={}, attachment_name='ADDON_ATTACHMENT_CUSTOM_NAME')

Remove an Addon:

addon = app.remove_addon(<id>)
addon = app.remove_addon(addonservice.id)
addon.delete()

Update/Upgrade an Addon:

addon = addon.upgrade(plan_id_or_name='<name>')
addon = addon.upgrade(plan_id_or_name='<id>')

Buildpacks

Update all buildpacks:

buildpack_urls = ['https://github.com/some/buildpack', 'https://github.com/another/buildpack']
app.update_buildpacks(buildpack_urls)

N.B. buildpack_urls can also be empty. This clears all buildpacks.

App Labs/Features

List all features:

appfeaturelist = app.features()
appfeaturelist = app.labs() #nicename for features()
appfeaturelist = app.features(order_by='id', limit=10)

Add a Feature:

appfeature = app.enable_feature(<feature_id_or_name>)

Remove a Feature:

appfeature = app.disable_feature(<feature_id_or_name>)

App Transfers

List all Transfers:

transferlist = app.transfers()
transferlist = app.transfers(order_by='id', limit=10)

Create a Transfer:

transfer = app.create_transfer(recipient_id_or_name=<user_id>)
transfer = app.create_transfer(recipient_id_or_name=<valid_email>)

Delete a Transfer:

deletedtransfer = app.delete_transfer(<transfer_id>)
deletedtransfer = transfer.delete()

Update a Transfer's state:

transfer.update(state)
transfer.update("Pending")
transfer.update("Declined")
transfer.update("Accepted")

Collaborators

List all Collaborators:

collaboratorlist = app.collaborators()
collaboratorlist = app.collaborators(order_by='id')

Add a Collaborator:

collaborator = app.add_collaborator(user_id_or_email=<valid_email>, silent=0)
collaborator = app.add_collaborator(user_id_or_email=user_id, silent=0)
collaborator = app.add_collaborator(user_id_or_email=user_id, silent=1) #don't send invitation email

Remove a Collaborator:

collaborator = app.remove_collaborator(userid_or_email)

ConfigVars

Get an apps config:

config = app.config()

Add a config Variable:

config['New_var'] = 'new_val'

Update a config Variable:

config['Existing_var'] = 'new_val'

Remove a config Variable:

del config['Existing_var']
config['Existing_var'] = None

Update Multiple config Variables:

# newconfig will always be a new ConfigVars object representing all config values for an app
# i.e. there won't be partial configs
newconfig = config.update({u'TEST1': u'A1', u'TEST2': u'A2', u'TEST3': u'A3'})
newconfig = heroku_conn.update_appconfig(<app_id_or_name>, {u'TEST1': u'A1', u'TEST2': u'A2', u'TEST3': u'A3'})
newconfig = app.update_config({u'TEST1': u'A1', u'TEST2': u'A2', u'TEST3': u'A3'})

Check if a var exists:

if 'KEY' in config:
    print("KEY = {0}".format(config[KEY]))

Get dict of config vars:

my_dict = config.to_dict()

Domains

Get a list of domains configured for this app:

domainlist = app.domains(order_by='id')

Add a domain to this app:

domain = app.add_domain('domain_hostname', 'sni_endpoint_id_or_name')
domain = app.add_domain('domain_hostname', None)  # domain will not be associated with an SNI endpoint

Example of finding a matching SNI, given a domain:

domain = 'subdomain.domain.com'
sni_endpoint_id = None
for sni_endpoint in app.sni_endpoints():
    for cert_domain in sni_endpoint.ssl_cert.cert_domains:
        # check root or wildcard
        if cert_domain in domain or cert_domain[1:] in domain:
            sni_endpoint_id_or_name = sni_endpoint.id
domain = app.add_domain(domain, sni_endpoint_id)

Remove a domain from this app:

domain = app.remove_domain('domain_hostname')

SNI Endpoints

Get a list of SNI Endpoints for this app:

sni_endpoints = app.sni_endpoints()

Add an SNI endpoint to this app:

sni_endpoint = app.add_sni_endpoint(
    '-----BEGIN CERTIFICATE----- ...',
    '-----BEGIN RSA PRIVATE KEY----- ...'
)

Update an SNI endpoint for this app:

sni_endpoint = app.update_sni_endpoint(
    'sni_endpoint_id_or_name',
    '-----BEGIN CERTIFICATE----- ...',
    '-----BEGIN RSA PRIVATE KEY----- ...'
)

Delete an SNI endpoint for this app:

app.remove_sni_endpoint('sni_endpoint_id_or_name')

Dynos & Process Formations

Dynos

Dynos represent all your running dyno processes. Use dynos to investigate whats running on your app. Use Dynos to create one off processes/run commands.

You don't "scale" dyno Processes. You "scale" Formation Processes. See Formations section Below

Get a list of running dynos:

dynolist = app.dynos()
dynolist = app.dynos(order_by='id')

Kill a dyno:

app.kill_dyno(<dyno_id_or_name>)
app.dynos['run.1'].kill()
dyno.kill()

Restarting your dynos is achieved by killing existing dynos, and allowing heroku to auto start them. A Handy wrapper for this proceses has been provided below.

N.B. This will only restart Formation processes, it will not kill off other processes.

Restart a Dyno:

#a simple wrapper around dyno.kill() with run protection so won't kill any proc of type='run' e.g. 'run.1'
dyno.restart()

Restart all your app's Formation configured Dyno's:

app.restart()

Run a command without attaching to it. e.g. start a command and return the dyno object representing the command:

dyno = app.run_command_detached('fab -l', size=1, env={'key': 'val'})
dyno = heroku_conn.run_command_on_app(<appname>, <command>, size=1, attach=False, printout=True, env={'key': 'val'})

Run a command and attach to it, returning the commands output as a string:

#printout  is used to control if the task should also print to STDOUT - useful for long running processes
#size = is the processes dyno size 1X(default), 2X, 3X etc...
#env = Envrionment variables for the dyno
output, dyno = heroku_conn.run_command_on_app(<appname>, <command>, size=1, attach=True, printout=True, env={'key': 'val'})
output = app.run_command('fab -l', size=1, printout=True, env={'key': 'val'})
print output

Formations

Formations represent the dynos that you have configured in your Procfile - whether they are running or not. Use Formations to scale dynos up and down

Get a list of your configured Processes:

proclist = app.process_formation()
proclist = app.process_formation(order_by='id')
proc = app.process_formation()['web']
proc = heroku_conn.apps()['myapp'].process_formation()['web']

Scale your Procfile processes:

app.process_formation()['web'].scale(2) # run 2 dynos
app.process_formation()['web'].scale(0) # don't run any dynos
proc = app.scale_formation_process(<formation_id_or_name>, <quantity>)

Resize your Procfile Processes:

app.process_formation()['web'].resize(2) # for 2X
app.process_formation()['web'].resize(1) # for 1X
proc = app.resize_formation_process(<formation_id_or_name>, <size>)

Log Drains

List all active logdrains:

logdrainlist = app.logdrains()
logdrainlist = app.logdrains(order_by='id')

Create a logdrain:

loggdrain = app.create_logdrain(<url>)

Remove a logdrain:

delete_logdrain - app.remove_logdrain(<id_or_url>)

Log Sessions

Access the logs:

log = heroku_conn.get_app_log(<app_id_or_name>, dyno='web.1', lines=2, source='app', timeout=False)
log = app.get_log()
log = app.get_log(lines=100)
print(app.get_log(dyno='web.1', lines=2, source='app'))
2011-12-21T22:53:47+00:00 heroku[web.1]: State changed from down to created
2011-12-21T22:53:47+00:00 heroku[web.1]: State changed from created to starting

You can even stream the tail:

#accepts the same params as above - lines|dyno|source|timeout (passed to requests)
log = heroku_conn.stream_app_log(<app_id_or_name>, lines=1, timeout=100)
#or
for line in app.stream_log(lines=1):
     print(line)

2011-12-21T22:53:47+00:00 heroku[web.1]: State changed from down to created
2011-12-21T22:53:47+00:00 heroku[web.1]: State changed from created to starting

Maintenance Mode

Enable Maintenance Mode:

app.enable_maintenance_mode()

Disable Maintenance Mode:

app.disable_maintenance_mode()

OAuth

OAuthAuthorizations

List all OAuthAuthorizations:

authorizations = heroku_conn.oauthauthorizations(order_by=id)

Get a specific OAuthAuthorization:

authorization = authorizations[<oauthauthorization_id>]
authorization = heroku_conn.oauthauthorization(oauthauthorization_id)

Create an OAuthAuthorization:

authorization = heroku_conn.oauthauthorization_create(scope, oauthclient_id=None, description=None)

Delete an OAuthAuthorization:

authorization.delete()
heroku_conn.oauthauthorization_delete(oauthauthorization_id)

OAuthClient

List all OAuthClients:

clients = heroku_conn.oauthclients(order_by=id)

Get a specific OAuthClient:

client = clients[<oauthclient_id>]
client = heroku_conn.oauthclient(oauthclient_id)

Create an OAuthClient:

client = heroku_conn.oauthclient_create(name, redirect_uri)

Update an existing OAuthClient:

client = client.update(name=None, redirect_uri=None)

Delete an OAuthClient:

client.delete()
heroku_conn.oauthclient_delete(oauthclient_id)

OAuthToken

Create an OAuthToken:

heroku_conn.oauthtoken_create(client_secret=None, grant_code=None, grant_type=None, refresh_token=None)

Release

List all releases:

releaselist = app.releases()
releaselist = app.releases(order_by='version')

Release information:

for release in app.releases():
    print("{0}-{1} released by {2} on {3}".format(release.id, release.description, release.user.name, release.created_at))

Rollback to a release:

app.rollback(release.id)
app.rollback("489d7ce8-1cc3-4429-bb79-7907371d4c0e")

Rename App

Rename App:

app.rename('Carrot-kettle-teapot-1898')

Customized Sessions

Heroku.py is powered by Requests and supports all customized sessions:

Logging

Note: logging is now achieved by the following method:

import httplib
httplib.HTTPConnection.debuglevel = 1

logging.basicConfig() # you need to initialize logging, otherwise you will not see anything from requests
logging.getLogger().setLevel(logging.INFO)
requests_log = logging.getLogger("requests.packages.urllib3")
requests_log.setLevel(logging.INFO)
requests_log.propagate = True

heroku_conn.ratelimit_remaining()

>>>INFO:requests.packages.urllib3.connectionpool:Starting new HTTPS connection (1): api.heroku.com
>>>send: 'GET /account/rate-limits HTTP/1.1\r\nHost: api.heroku.com\r\nAuthorization: Basic ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ=\r\nContent-Type: application/json\r\nAccept-Encoding: gzip, deflate, compress\r\nAccept: application/vnd.heroku+json; version=3\r\nUser-Agent: python-requests/1.2.3 CPython/2.7.2 Darwin/12.4.0\r\n\r\n'
>>>reply: 'HTTP/1.1 200 OK\r\n'
>>>header: Content-Encoding: gzip
>>>header: Content-Type: application/json;charset=utf-8
>>>header: Date: Thu, 05 Sep 2013 11:13:03 GMT
>>>header: Oauth-Scope: global
>>>header: Oauth-Scope-Accepted: global identity
>>>header: RateLimit-Remaining: 2400
>>>header: Request-Id: ZZZZZZ2a-b704-4bbc-bdf1-e4bc263586cb
>>>header: Server: nginx/1.2.8
>>>header: Status: 200 OK
>>>header: Strict-Transport-Security: max-age=31536000
>>>header: Vary: Accept-Encoding
>>>header: X-Content-Type-Options: nosniff
>>>header: X-Runtime: 0.032193391
>>>header: Content-Length: 44
>>>header: Connection: keep-alive

Installation

To install heroku3.py, simply:

$ pip install heroku3

Or, if you absolutely must:

$ easy_install heroku3

But, you really shouldn't do that.

License

Original Heroku License left intact, The code in this repository is mostly my own, but credit where credit is due and all that :)

Copyright (c) 2013 Heroku, Inc.

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

heroku3.py's People

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar

heroku3.py's Issues

Dyno restart doesn't work

Hi,

the below Heroku API doesn't work if it is called with dyno id but works fine if it is called with dyno name. The library call this API with dyno id so this functionality doesn't work now.

DELETE /apps/{app_id_or_name}/dynos/{dyno_id_or_name}

BaseResource._keys() method does not return dict attributes.

The method BaseResource._key() does not include the attributes decalred in BaseResource._dicts.

Is that intentional or not ? Chaiging this would impact the following methods:

That latter method seem to be dead code: never called anywhere as far as grep can tell, but then it is also part of the class public AP, so removing it may not be the best idea. But if we agree the omission of dict attributes is a bug, then it should be fixed.

Any clue ?
Regards, Nicolas.

Issues when listing objects + misc issues

Hey there,

Thanks for the great wrapper! I stumbled upon a few things while using it to automate Heroku:

  • It would be useful if the App object had an organization object with id and name just as the API returns.

  • When the list of addons is empty, app.addons() should be False to enable if not app.addons() checks. Right now, an empty addon list is evaluated as True.

    Generally speaking, KeyedListResource should probably implement __len__

  • Passing name to app.addons() does not work to filter / find a specific addon -- the name keyword is ignored.

    i.e., app.addons(name='bleh') will return the full list of addons

Thank you!

Authorization Fails using Token

We used to authenticate using:

auth_token = subprocess.check_output(['heroku', 'auth:token'])
heroku = heroku3.from_key(auth_token)

This started failing recently (maybe a month ago?). Digging in, it seems like Heroku only supports a Bearer <Token> Authorization header (https://devcenter.heroku.com/articles/platform-api-quickstart#authentication). I don't see any reference to a Basic Authorization header - My best guess is that it was supported, but has been deprecated?

I was able to work around this and successfully authenticate with the following bit of code:

class BearerAuth(requests.auth.AuthBase):
    """ Token based authentication """

    def __init__(self, token):
        self.token = token

    def __eq__(self, other):
        return self.token == getattr(other, 'token', None)

    def __ne__(self, other):
        return not self == other

    def __call__(self, r):
        r.headers['Authorization'] = 'Bearer %s' % self.token
        return r


class Heroku31(heroku3.api.Heroku):
    """ Monkey patched Heroku3 - Heroku doesn't accept Basic Auth anymore """

    def authenticate(self, api_key):
        """Logs user into Heroku with given api_key."""
        self._api_key = api_key

        # Attach auth to session.
        self._session.auth = BearerAuth(self._api_key)

        return self._verify_api_key()

I'm kind of surprised nobody else has run into this - so, maybe we were just doing something wrong from the start?

Timeout Cannot Be Boolean

  File "/root/TeamUltroid/plugins/devtools.py", line 132, in _
    await aexec(cmd, event)
  File "/root/TeamUltroid/plugins/devtools.py", line 181, in aexec
    return await locals()["__aexec"](event, event.client)
  File "<string>", line 7, in __aexec
  File "/usr/local/lib/python3.9/site-packages/heroku3/api.py", line 526, in get_app_log
    return logger.get(timeout=timeout)
  File "/usr/local/lib/python3.9/site-packages/heroku3/models/logsession.py", line 27, in get
    r = requests.get(self.logplex_url, verify=False, stream=True, timeout=timeout)
  File "/usr/local/lib/python3.9/site-packages/requests/api.py", line 76, in get
    return request('get', url, params=params, **kwargs)
  File "/usr/local/lib/python3.9/site-packages/requests/api.py", line 61, in request
    return session.request(method=method, url=url, **kwargs)
  File "/usr/local/lib/python3.9/site-packages/requests/sessions.py", line 542, in request
    resp = self.send(prep, **send_kwargs)
  File "/usr/local/lib/python3.9/site-packages/requests/sessions.py", line 655, in send
    r = adapter.send(request, **kwargs)
  File "/usr/local/lib/python3.9/site-packages/requests/adapters.py", line 435, in send
    timeout = TimeoutSauce(connect=timeout, read=timeout)
  File "/usr/local/lib/python3.9/site-packages/urllib3/util/timeout.py", line 103, in __init__
    self._connect = self._validate_timeout(connect, "connect")
  File "/usr/local/lib/python3.9/site-packages/urllib3/util/timeout.py", line 137, in _validate_timeout
    raise ValueError(
ValueError: Timeout cannot be a boolean value. It must be an int, float or None.

no support for pg?

It doesn't seem like this library supports any postgres actions. Like heroku pg:info. Is that correct?

Detect if worker-scaling is done

I am trying to scale up a worker to handle a background job, and when the job is done, scale down the worker, as follows:

apiKey = '...'
appName = '...'

# Start a worker
heroku_conn = heroku3.from_key(apiKey)
app = heroku_conn.apps()[appName]
app.process_formation()['worker'].scale(1)

# Enqueue the background job
# time.sleep(10) --> How do I know if the worker is ready?
q = Queue(connection=conn)
q.enqueue(backgroundJob)

In another file:

def backgroundJob():
    # This is the background job
    ...
    # Turn off the worker
    app.process_formation()['worker'].scale(0)

My question: How do I know if the worker is ready? The following code doesn't seem to work:

while 'worker.1' not in app.dynos():
    time.sleep(1)

Thanks.

run dyno

can I run dyno of applications using this module?

Update information on resources

Before I possibly dove into a PR for this, I was wondering if there was functionality to update the information on a resource (process formation, dyno, etc), ie:

h = heroku3.from_key(API_KEY)
app = h.apps()[APP_NAME]
formation = app.process_formation()
web = formation['web']
print(web.quantity)
>>> 1
# time passes, a sleep, or what have you
web.some_update_method()
print(web.quantity)
>>> 3

because constantly using:

app.process_formation()['web'].quantity

is just silly, I should be able to update just the resource i want.


Maybe a more relevant example:

new_dyno = app.run_process_detached(SOME_COMMAND)
print new_dyno.state
>>> u'starting'
# Some time passes, I start other dynos, enter a sleep loop
new_dyno.some_update_command()
print new_dyno.state
>>> u'shutdown' # I don't actually know what this status is

Presently, the only way I know how to do this with a dyno is to attempt a lookup on the .dynos() method:

new_dyno = app.run_process_detached(SOME_COMMAND)
print new_dyno.state
>>> u'starting'
# Time passes
try:
    app.dynos()[new_dyno.id]
    print('still running')
except KeyError:
    print('dyno stopped')

Which is, again, silly. Please let me know if there's already a way to do this (I couldn't find it in the documentation), if not I'll try to find cycles to attempt this.

Tiny spelling

In the examples replace maintence with maintenance

Domain Create endpoint API has changed

The Domain Create endpoint now requires an sni_endpoint parmeter: https://devcenter.heroku.com/articles/platform-api-reference#domain-create

This means that calls to app.add_domain(...) are now raising 422 errors.

The solution is to update the API to add the sni_endpoint parameter - also need to figure out exactly what SNI endpoint needs to be passed, and if/how that plays with Heroku's Automated Certificate Management (ACM).

I'm happy to look into this and make a PR for it over the next week or so.

Problems with getting output of run_command

I would like to run run_command and capture the output in a string. For example:

output = app.run_command('otree resetdb --noinput')
print(output)

I want output like this, which is what I would see in my terminal if I execute the command "heroku run":

C:\oTree\tmp5 [master]> heroku run otree resetdb --noinput
Running otree resetdb --noinput on โฌข pure-sands-31684... up, run.2610 (Free)
INFO Database engine: PostgreSQL
INFO Retrieving Existing Tables...
INFO Dropping Tables...
INFO Creating Database 'default'...
Operations to perform:
  Apply all migrations: (none)
Synchronizing apps without migrations:
  Creating tables...
    Creating table otree_chatmessage
    Creating table otree_session
    Creating table otree_participant
    Creating table auth_permission
    Creating table auth_group
    Creating table auth_user
    Creating table django_content_type
    Creating table django_session
    [...]
    Running deferred SQL...
Running migrations:
  No migrations to apply.
INFO Created new tables and columns.

However, in actuality I get an error:

---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-16-b0c13e97288e> in <module>
----> 1 output = app.run_command('otree resetdb --noinput')
      2 print(output)

c:\otree\ve_manager\lib\site-packages\heroku3\models\app.py in run_command(self, command, attach, printout, size, env)
    238 
    239         if attach:
--> 240             output = Rendezvous(dyno.attach_url, printout).start()
    241             return output, dyno
    242         else:

c:\otree\ve_manager\lib\site-packages\heroku3\rendezvous.py in start(self)
     40         ssl_sock.settimeout(20)
     41         ssl_sock.connect((self.hostname, self.port))
---> 42         ssl_sock.write(self.secret)
     43         data = ssl_sock.read()
     44         if not data.startswith("rendezvous"):

~\AppData\Local\Programs\Python\Python37\lib\ssl.py in write(self, data)
    925         if self._sslobj is None:
    926             raise ValueError("Write on closed or unwrapped SSL socket.")
--> 927         return self._sslobj.write(data)
    928 
    929     def getpeercert(self, binary_form=False):

TypeError: a bytes-like object is required, not 'str'

My first thought was to change the command to a byte string:

output = app.run_command(b'otree resetdb --noinput')
print(output)

But that gives me the opposite error:

TypeError                                 Traceback (most recent call last)
<ipython-input-21-abda4946c33d> in <module>
----> 1 output = app.run_command(b'otree resetdb --noinput')
      2 print(output)

c:\otree\ve_manager\lib\site-packages\heroku3\models\app.py in run_command(self, command, attach, printout, size, env)
    230             method='POST',
    231             resource=('apps', self.name, 'dynos'),
--> 232             data=self._h._resource_serialize(payload)
    233         )
    234 

c:\otree\ve_manager\lib\site-packages\heroku3\api.py in _resource_serialize(o)
     99     def _resource_serialize(o):
    100         """Returns JSON serialization of given object."""
--> 101         return json.dumps(o)
    102 
    103     @staticmethod

~\AppData\Local\Programs\Python\Python37\lib\json\__init__.py in dumps(obj, skipkeys, ensure_ascii, check_circular, allow_nan, cls, indent, separators, default, sort_keys, **kw)
    229         cls is None and indent is None and separators is None and
    230         default is None and not sort_keys and not kw):
--> 231         return _default_encoder.encode(obj)
    232     if cls is None:
    233         cls = JSONEncoder

~\AppData\Local\Programs\Python\Python37\lib\json\encoder.py in encode(self, o)
    198         # exceptions aren't as detailed.  The list call should be roughly
    199         # equivalent to the PySequence_Fast that ''.join() would do.
--> 200         chunks = self.iterencode(o, _one_shot=True)
    201         if not isinstance(chunks, (list, tuple)):
    202             chunks = list(chunks)

~\AppData\Local\Programs\Python\Python37\lib\json\encoder.py in iterencode(self, o, _one_shot)
    256                 self.key_separator, self.item_separator, self.sort_keys,
    257                 self.skipkeys, _one_shot)
--> 258         return _iterencode(o, 0)
    259 
    260 def _make_iterencode(markers, _default, _encoder, _indent, _floatstr,

~\AppData\Local\Programs\Python\Python37\lib\json\encoder.py in default(self, o)
    178         """
    179         print('@@@o is', o)
--> 180         raise TypeError(f'Object of type {o.__class__.__name__} '
    181                         f'is not JSON serializable')
    182 

TypeError: Object of type bytes is not JSON serializable

I read in someone else's issue that they fixed this by passing attach=False, so I tried that, even though it's not clear to me why that is related. Anyway, the error is gone, but all I get is this:

<Dyno 'run.8868 - otree resetdb --noinput'>

I get the same above output even if I pass printout=True:

output = app.run_command('otree resetdb --noinput', attach=False, printout=True)
print(output)

I tried copying directly a code example from the docs:

output = app.run_command('fab -l', size=1, printout=True, env={'key': 'val'})
print(output)

I also get an error:

---------------------------------------------------------------------------
HTTPError                                 Traceback (most recent call last)
<ipython-input-18-4c7e7f2a1781> in <module>
      8 #app.run_command('fab -l', printout=True)
      9 #output = app.run_command_detached('fab -l')
---> 10 output = app.run_command('fab -l', size=1, printout=True, env={'key': 'val'})
     11 print(output)

c:\otree\ve_manager\lib\site-packages\heroku3\models\app.py in run_command(self, command, attach, printout, size, env)
    230             method='POST',
    231             resource=('apps', self.name, 'dynos'),
--> 232             data=self._h._resource_serialize(payload)
    233         )
    234 

c:\otree\ve_manager\lib\site-packages\heroku3\api.py in _http_resource(self, method, resource, params, data, legacy, order_by, limit, valrange, sort)
    175                                    (self._last_request_id, r.status_code, r.content.decode("utf-8")))
    176             http_error.response = r
--> 177             raise http_error
    178 
    179         if r.status_code == 429:

HTTPError: 59698995-8c24-4251-a70b-121f53e95a38,4df5b8fc-c03a-6ed2-05eb-1e018dc753ce,98f3f4a5-0072-bee3-b78c-c79e7ab24e1c - 422 Client Error: {"id":"invalid_params","message":"Unable to process request with specified parameters."}

How can I easily get the output that I would see if running heroku run otree resetdb --noinput?

Pypi release?

Hi, I've added a new feature by PR and I'd quite like to use it :P

Plan price returns None object.

Hello,

I'm running python 2.7.6

I'm trying to loop through all apps and get the price for each addon associated with the app to calculate the total cost of the app. I thought the code below should work but plan.price is None:

for app in conn.apps():
        price_breakdown = []

        addon_list = app.addons()
        for addon in addon_list:
              plan = addon.plan
              price_breakdown.append((
                '{}@{}'.format(app.name, plan.name),
                plan.price.cents))

I get the following trace:

  File "/vagrant/tasks.py", line 129, in check_prices
    plan.price.cents))
AttributeError: 'NoneType' object has no attribute 'cents'

Any thoughts on how I can get the pricing info for each app addon?
Thank you.

Exception with Python2.7.15

Traceback (most recent call last):
  File "/venv/lib/python2.7/site-packages/heroku3/__init__.py", line 27, in <module>
    from .core import from_key
  File "/venv/lib/python2.7/site-packages/heroku3/core.py", line 12, in <module>
    from .api import Heroku
  File "/venv/lib/python2.7/site-packages/heroku3/api.py", line 22, in <module>
    from .models.app import App
  File "/venv/lib/python2.7/site-packages/heroku3/models/app.py", line 6, in <module>
    from ..rendezvous import Rendezvous
  File "/venv/lib/python2.7/site-packages/heroku3/rendezvous.py", line 7, in <module>
    from six.moves.urllib.parse import urlparse, uses_netloc
ImportError: cannot import name uses_netloc

Running into this issue on Python 2.7.15.
Any suggested course of action?

Wrong "python-dateutil" version

Currently pip heroku3 will automatically install the following:

heroku3==3.2.0
python-dateutil==1.5
requests==2.12.0
simplejson==3.3.1

The latest version of python-dateutil is 2.6.0, and version 1.5 will cause run time errors.

Exception with Python 2.7.12

It seems like the version of RendezVous included isn't Python 2.X compatible:

"""
File "heroku3/init.py", line 27, in
from .core import from_key
File "heroku3/core.py", line 10, in
from .api import Heroku
File "heroku3/api.py", line 13, in
from .models.app import App
File "heroku3/models/app.py", line 2, in
from ..rendezvous import Rendezvous
File "heroku3/rendezvous.py", line 6, in
from urllib.parse import urlparse, uses_netloc
ImportError: No module named parse
"""

Broken error handling

Error handling is broken. For example,

a = heroku_conn.create_app(name='test-app-s1', organization='XXXX')
Traceback (most recent call last):
File "/private/tmp/venv3/lib/python3.6/site-packages/heroku3/api.py", line 291, in create_app
data=self._resource_serialize(payload)
File "/private/tmp/venv3/lib/python3.6/site-packages/heroku3/api.py", line 177, in _http_resource
raise http_error
requests.exceptions.HTTPError: 7ec843b4-6ae3-47fa-512b-6526297d401c,fb365328-849f-5397-fe1e-2a7540ec7d7d,0659f6a0-7a06-2848-acbc-4cc0ce9c7207 - 422 Client Error: {"id":"invalid_params","message":"Name is already taken"}

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
File "", line 1, in
File "/private/tmp/venv3/lib/python3.6/site-packages/heroku3/api.py", line 298, in create_app
print("Warning - {0:s}".format(e))
TypeError: unsupported format string passed to HTTPError.format

In heroku3/api.py, use str(e) in print("Warning - {0:s}".format(e))

KeyError while calling update_appconfig()

>>> import os
>>> import heroku3
>>> heroku_conn = heroku3.from_key(os.environ['HEROKU_API_KEY'])
>>> updated_config = heroku_conn.update_appconfig(app_id_or_name='visesh', config={'DEBUG':'True'})
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/....../virtualenvs/pyenv/lib/python3.7/site-packages/heroku3/api.py", line 502, in update_appconfig
    return ConfigVars.new_from_dict(item, h=self)
  File "/....../virtualenvs/pyenv/lib/python3.7/site-packages/heroku3/models/configvars.py", line 107, in new_from_dict
    c = cls(d, kwargs.pop('app'), h=h, **kwargs)
KeyError: 'app'

ValueError: Timeout cannot be a boolean value. It must be an int, float or None.

Hi! I'm using the next code to get the log from my heroku app:

app = heroku_conn.apps()['aurorabugsbunny']
log = app.get_log(lines=100)

But I ended up with this error when I try to use the log variable:

File "/app/.heroku/python/lib/python3.6/site-packages/telegram/ext/dispatcher.py", line 279, in process_update

2018-08-29T14:31:38.120073+00:00 app[worker.1]: handler.handle_update(update, self)

2018-08-29T14:31:38.120075+00:00 app[worker.1]: File "/app/.heroku/python/lib/python3.6/site-packages/telegram/ext/commandhandler.py", line 173, in handle_update

2018-08-29T14:31:38.120077+00:00 app[worker.1]: return self.callback(dispatcher.bot, update, **optional_args)

2018-08-29T14:31:38.120079+00:00 app[worker.1]: File "/app/bot/remote.py", line 26, in remote

2018-08-29T14:31:38.120080+00:00 app[worker.1]: log = app.get_log(dyno='worker.1', lines=100, source='aurorabugsbunny')

2018-08-29T14:31:38.120082+00:00 app[worker.1]: File "/app/.heroku/python/lib/python3.6/site-packages/heroku3/models/app.py", line 467, in get_log

2018-08-29T14:31:38.120084+00:00 app[worker.1]: return logger.get(timeout=timeout)

2018-08-29T14:31:38.120086+00:00 app[worker.1]: File "/app/.heroku/python/lib/python3.6/site-packages/heroku3/models/logsession.py", line 25, in get

2018-08-29T14:31:38.120088+00:00 app[worker.1]: r = requests.get(self.logplex_url, verify=False, stream=True, timeout=timeout)

2018-08-29T14:31:38.120090+00:00 app[worker.1]: File "/app/.heroku/python/lib/python3.6/site-packages/requests/api.py", line 72, in get

2018-08-29T14:31:38.120091+00:00 app[worker.1]: return request('get', url, params=params, **kwargs)

2018-08-29T14:31:38.120093+00:00 app[worker.1]: File "/app/.heroku/python/lib/python3.6/site-packages/requests/api.py", line 58, in request

2018-08-29T14:31:38.120095+00:00 app[worker.1]: return session.request(method=method, url=url, **kwargs)

2018-08-29T14:31:38.120096+00:00 app[worker.1]: File "/app/.heroku/python/lib/python3.6/site-packages/requests/sessions.py", line 512, in request

2018-08-29T14:31:38.120098+00:00 app[worker.1]: resp = self.send(prep, **send_kwargs)

2018-08-29T14:31:38.120100+00:00 app[worker.1]: File "/app/.heroku/python/lib/python3.6/site-packages/requests/sessions.py", line 622, in send

2018-08-29T14:31:38.120101+00:00 app[worker.1]: r = adapter.send(request, **kwargs)

2018-08-29T14:31:38.120103+00:00 app[worker.1]: File "/app/.heroku/python/lib/python3.6/site-packages/requests/adapters.py", line 431, in send

2018-08-29T14:31:38.120105+00:00 app[worker.1]: timeout = TimeoutSauce(connect=timeout, read=timeout)

2018-08-29T14:31:38.120106+00:00 app[worker.1]: File "/app/.heroku/python/lib/python3.6/site-packages/urllib3/util/timeout.py", line 94, in init

2018-08-29T14:31:38.120108+00:00 app[worker.1]: self._connect = self._validate_timeout(connect, 'connect')

2018-08-29T14:31:38.120110+00:00 app[worker.1]: File "/app/.heroku/python/lib/python3.6/site-packages/urllib3/util/timeout.py", line 121, in _validate_timeout

2018-08-29T14:31:38.120111+00:00 app[worker.1]: raise ValueError("Timeout cannot be a boolean value. It must "

2018-08-29T14:31:38.120120+00:00 app[worker.1]: ValueError: Timeout cannot be a boolean value. It must be an int, float or None.

Running multiple one-off-dyno commands

I am trying to run two (or more) consecutive jobs on one-off dynos using heroku3 as follows:

import heroku3

heroku_conn = heroku3.from_key(apiKey)
app = heroku_conn.apps()[appName]
app.run_command_detached(command1)

heroku_conn = heroku3.from_key(apiKey)
app = heroku_conn.apps()[appName]
app.run_command_detached(command2)

Question:
Supposing only one one-off dyno is allowed (for the free option) on Heroku, which of the followings would happen?

1. Run both jobs *concurrently* on the same dyno
2. Run the first one, and then the second one after the first finishes
3. Run the first one, but not the second one.
4. Overall, what's the better way of running *consecutive* jobs using one-off dynos?

Thanks.

[Question] Why is `BaseResource._ids` yielding primary keys twice ?

The BaseResource._ids property yields all so called primary keys twice, once as the value of the attribute itself, once as the value of the attribute cast to a string with str.

Several questions about that:

  • Why doing so ? What is the use case ?
  • If there is a use case indeed, nothing prevents me to use an array, dict or object attribute as a key. What would be the meaning of str(object) ?

Not convinced that works well and since it is not used anywhere in the code, not sure there actually is a need for this.

Also as a side note, I think the name _pks is very much misleading. An entity can only have several keys but only one primary key (which is where I guess pk in _pks is coming from).

I welcome any feedback on this. Thanks in advance.

Support for setting app limits (boot_timeout specifically)

I'd like to replace a HTTP call to Heroku APIs to set the boot_timeout parameter of an app with a call to this package.

The API endpoint is:
https://api.heroku.com/apps/<app_name>/limits/boot_timeout

I couldn't find any reference to it, so I'm wondering if it is at all supported or needs a PR?

Get collaborators permissions?

Hello! Is there a way to get collaborators permissions/role for an app? For example, I'm looking to list all the collaborators for apps with "Manage" permission.

Thanks!

Can't get the domain info

heroku3.py version: v3.2.2

How can I get the the hostname/domain info ?

As Heroku Official Docs says https://devcenter.heroku.com/articles/platform-api-reference#domain-info the request should be constructed as:

$ curl -n https://api.heroku.com/apps/$APP_ID_OR_NAME/domains/$DOMAIN_ID_OR_HOSTNAME \
  -H "Accept: application/vnd.heroku+json; version=3"

On heroku3.py I only find App.domain which accepts a **kwargs and even though I pass hostname=somedomain.com to it, it just returns the list of the domains on the app.

for instance:

In [9]: app.domains()
Out[9]: [<domain '*.draft.gonevis.com'>, <domain 'draft.gonevis.com'>, <domain 'gonevis-draft.herokuapp.com'>]

# When passing hostname
In [25]: app.domains(hostname='helooooo')
Out[25]: [<domain 'helooooo'>, <domain 'helooooo'>, <domain 'helooooo'>]

# When passing a valid existing domain
In [26]: app.domains(hostname='draft.gonevis.com')
Out[26]: [<domain 'draft.gonevis.com'>, <domain 'draft.gonevis.com'>, <domain 'draft.gonevis.com'>]

That behavior seems to be bug.
Or am I doing something wrong ?

app.process_formation() reporting empty KeyedListResource

Some app formations are returning an empty KeyedListResource list, rather than 'job' or 'web' or something similar. Any idea what is causing this behavior?

I have confirmed these formations have valid types by manual curl calls directly to the Heroku API.

python-dateutil version restriction causing dependency conflicts

Hi there,

Is there a reason heroku3 needs the exact version for python-dateutil==2.6.0?
We get an error when installing after other packages which install a later version.

error: python-dateutil 2.7.2 is installed but python-dateutil==2.6.0 is required by set(['heroku3'])

I imagine both simplejson and python-dateutil will maintain backward compatibility, so could the install-requires be changed to this?

required = [
    'requests>=1.2.3',
    'simplejson>=3.3.1',
    'python-dateutil>=2.6.0',
]

Start and top the one-off dyno

I am using heroku3 to start a one-off dyno and then stop it when the job is done.

# Start a one-off dyno
apiKey = 'xxxx...'
appName = 'yyyy...'
heroku_conn = heroku3.from_key(apiKey)
app = heroku_conn.apps()[appName]
if 'run.1' in app.dynos:
    print('Another one-off dyno is running.')
else:
    app.run_command_detached('python -m theModuleToRun')\

"theModuleToRun.py":
# Perform some job
# ...
# Stop the one-off dyno when the job is done
apiKey = 'xxxx...'
appName = 'yyyy...'
heroku_conn = heroku3.from_key(apiKey)
app = heroku_conn.apps()[appName]
if 'run.1' in app.dynos:
    app.dynos['run.1'].kill()

My question:
Is the code to start and stop the dyno correct? Is the name of the one-off dyno always run.1, given that there is only one one-off dyno running at the same time?

App.add_domain() returns unnecessary 422

This corresponds to a support ticket I've opened.

Using the latest pypi release for this package (3.3.0) I run the following:

import heroku3
from django.conf import settings

heroku_conn = heroku3.from_key(settings.HEROKU_KEY)

# returns error:
# <really-long-uuid-here> 422 Client Error: {"id":"invalid_params","message":"Domain already added to this app."}
heroku_conn.apps()['my-app'].add_domain('somedomainthatdefinitelydoesntexistonthisappyet.com')

It would appear as though it's submitting the domain addition request twice, because the domain is added to the app as specified. So the first request adds the domain and returns a 200-level status, but the duplicate request returns 400 and raises an error within my app logic. Unfortunately I can't just catch and ignore this error because in the event an actual duplicate domain is submitted my app logic needs to handle the error gracefully.

See App.add_domain

Fails on Python 3.5

>>> import heroku3
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File ".../lib/python3.5/site-packages/heroku3/__init__.py", line 27, in <module>
    from .core import from_key
  File ".../lib/python3.5/site-packages/heroku3/core.py", line 10, in <module>
    from .api import Heroku
  File ".../lib/python3.5/site-packages/heroku3/api.py", line 203
    print "Warning Response was chunked, Loading the next Chunk using the following next-range header returned by Heroku '{0}'. WARNING - This breaks randomly depending on your order_by name. I think it's only guarenteed to work with id's - Looks to be a Heroku problem".format(valrange)

Support for org/team membership queries?

I'm new to Heroku, and this library, so I might be missing how to access this. The api documents access to an organization's membership (and attributes like '2fa enabled').

I don't see how to access that part of the API using heroku3 -- what am I missing?

invalid Range header for sort

When I try to do the "list releases" example in the README:

app.releases(order_by='version', limit=10, sort='desc')

the API responds with an error:

{
  "message": "Invalid `Range` header. Please use format like `id ..; max=10, order=desc`. ", 
  "id": "bad_request"
}

The Range header in the request is:

version ..; max=10; order=desc

semicolon instead of a comma?

InsecureRequestWarning with LogSession

When calling get_log(), we get the following warning:

/lib/python2.7/site-packages/urllib3/connectionpool.py:858: InsecureRequestWarning: Unverified HTTPS request is being made. Adding certificate verification is strongly advised. See: https://urllib3.readthedocs.io/en/latest/advanced-usage.html#ssl-warnings
InsecureRequestWarning)
-- Docs: http://doc.pytest.org/en/latest/warnings.html

This warning doesn't happen with other API methods, only LogSession.

Thanks for looking into this.

Wrong parameter passed to `ConfigVars.new_from_dict()` in `ConfigVars.update()`

In the method ConfigVars.update() a new ConfigVars instance is built, with the values returned by the heroku API, and returned to the user. Yet that instance is not valid as the h keyword argument receives a ConfigVars instance instead of the expected Heroku instance (passed self instead of self._h).

Given that:

  • this issue has not been reported before (meaning most likely no one ever used the return value of that method in its code);
  • this class tries very hard to look like a dict;
  • the dict.update() method has a semantic that mutates the dictionary state in place and returns None (as all function / method mutating built-in types do in Python);

I would suggest that the API be modified as follows: the state of the ConfigVars instance should mutate it's internal dictionary (self.data) upon successful response from Heroku's API, and the method returns None.

Enable non API-key auth mecanism

It might be convinient to allow authentication mecanisms other than "api key". E.g. for people using heroku's cli tool, taking advantage of the requests library ability to read .netrc files (as noted here) could be nice.

Several issues prevent that:

simplejson dependency is pinned to version 3.3.1

I'm using the version 3.4.0 of heroku3 and 1 of the requirement is simplejson which is pinned to version simplejson==3.3.1.

It's problematic because it conflicts with other packages using a higher version number, in my case simplejson>=3.16.0.

Would it be possible to use simplejson>=3.3.1,<4 ?

create_app may throw unexpectedly

app = self.app(name)
: app = self.app(name) may throw unexpectedly: requests.exceptions.HTTPError: 403 Client Error: Forbidden for url: xxx

In the except clause above, we are trying to recover from an exception when the app name requested is already in use. The problem occurs when the app name requested is in use by a different heroku user. App names are required to be unique across users, presumably because the app URL needs to be unique. In that scenario, an exception 422 Client Error: {"id":"invalid_params","message":"Name is already taken"} is thrown, converted into a warning and we attempt to return a reference to the app. This works if the app is owned by the same user that is trying to create the new app with the existing name, but fails otherwise, in which case, the error/exception is a bit cryptic.

A solution could be:

        except HTTPError as e:
            saved_exc = (sys.exc_type, sys.exc_value, sys.exc_traceback)
            if "Name is already taken" in str(e):
                try:
                    app = self.app(name)
                    print("Warning - {0:s}".format(e))
                except:
                    raise saved_exc[0], saved_exc[1], saved_exc[2]
            else:
                raise saved_exc[0], saved_exc[1], saved_exc[2]

Using raise saved_exc[0], saved_exc[1], saved_exc[2] instead of raise e re-raises the exception as if it had not been handled, that way the stack-trace is not modified and it likely results in it being more informative. Saving the exception values in saved_exc is necessary; we cannot simply use raise without an argument, because if the line app = self.app(name) raises and we subsequently use raise without an argument, we will re-raise that inner exception.

Missing dependency on future

Since PR #47 has been merged, heroku3 depends on the future package, yet the dependency is not declared in the requirements.txt file.

Instead of pulling a new dependency, I would have used plain Python 2 and 3 compatible code. Not trying to start anything, just would like to have the most future proof code possible. The following construct works as far as I could remember.

try:
     # raise
except <ExceptionClass>:
    raise

Output may not be 100% similar depending on the Python version but it is way more future proof (in some Python version the stack trace may get lost).

cans@workstation:~/src/gitco/ansible/heroku3.py$ python3
Python 3.6.3 (default, Oct  3 2017, 21:45:48) 
[GCC 7.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> try:
...     raise Exception('message')
... except Exception:
...     raise
... 
Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
Exception: message
>>> 

cans@workstation:~$ python3.5
Python 3.5.3 (default, Jan 19 2017, 14:11:04) 
[GCC 6.3.0 20170118] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> try:
...     raise Exception('message')
... except Exception:
...     raise
... 
Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
Exception: message
>>> 

cans@workstation:~/src/gitco/ansible/heroku3.py$ python2
Python 2.7.14 (default, Sep 23 2017, 22:06:14) 
[GCC 7.2.0] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> try:
...     raise Exception('message')
... except Exception:
...     raise
... 
Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
Exception: message
>>> 

Sorry don't have any other python version availble. Don't remember if 2.6 supports this but it apparently should. Can provide a poor man's fix (add missing requirement) or implement the solution above. But I don't feel very confortable with that since this package includes not tests.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.