Giter VIP home page Giter VIP logo

hubspot3's Introduction

PyPI version Code style: black Documentation Status Python 3.8+ supported

A python wrapper around HubSpot's APIs, for python 3.8+.

Built initially around hapipy, but heavily modified.

Check out the documentation here! (thanks readthedocs)

Quick start

Note: I'd recommend that you consider using the official HubSpot Python API. I no longer work at a company that uses HubSpot, and this library is a few versions behind on many of their APIs. I will be open to small PRs and usability fixes, but there will likely not be any further significant work on this library

Installation

# install hubspot3
pip install hubspot3

Basic Usage

from hubspot3 import Hubspot3

API_KEY = "your-api-key"

client = Hubspot3(api_key=API_KEY)

# all of the clients are accessible as attributes of the main Hubspot3 Client
contact = client.contacts.get_contact_by_email('[email protected]')
contact_id = contact['vid']

all_companies = client.companies.get_all()

# new usage limit functionality - keep track of your API calls
client.usage_limits
# <Hubspot3UsageLimits: 28937/1000000 (0.028937%) [reset in 22157s, cached for 299s]>

client.usage_limits.calls_remaining
# 971063

Individual Clients

from hubspot3.companies import CompaniesClient

API_KEY = "your-api-key"

client = CompaniesClient(api_key=API_KEY)

for company in client.get_all():
    print(company)

Passing Params

import json
from hubspot3.deals import DealsClient

deal_id = "12345"
API_KEY = "your_api_key"

deals_client = DealsClient(api_key=API_KEY)

params = {
    "includePropertyVersions": "true"
}  # Note values are camelCase as they appear in the Hubspot Documentation!

deal_data = deals_client.get(deal_id, params=params)
print(json.dumps(deal_data))

Command-line interface

There is also a command-line tool available. Install the extra requirement for that tool via:

pip install hubspot3[cli]

and you can use it as a command:

hubspot3 --help

See the Sphinx documentation for more details and explanations.

Rate Limiting

Be aware that this uses the HubSpot API directly, so you are subject to all of the guidelines that HubSpot has in place.

at the time of writing, HubSpot has the following limits in place for API requests:

Free & Starter:

  • 10 requests per second
  • 250,000 requests per day.

Professional & Enterprise:

  • 10 requests per second
  • 500,000 requests per day.

This daily limit resets at midnight based on the time zone setting of the HubSpot account. There is also an additional addon you can purchase for more requests.

Retrying API Calls

By default, hubspot3 will attempt to retry all API calls up to 2 times upon failure.

If you'd like to override this behavior, you can add a number_retries keyword argument to any Client constructor, or to individual API calls.

Extending the BaseClient - thanks @Guysoft!

Some of the APIs are not yet complete! If you'd like to use an API that isn't yet in this repo, you can extend the BaseClient class!

import json
from hubspot3.base import BaseClient


PIPELINES_API_VERSION = "1"


class PipelineClient(BaseClient):
    """
    Lets you extend to non-existing clients, this example extends pipelines
    """

    def __init__(self, *args, **kwargs):
        super(PipelineClient, self).__init__(*args, **kwargs)

    def get_pipelines(self, **options):
        params = {}

        return self._call("pipelines", method="GET", params=params)

    def _get_path(self, subpath):
        return f"deals/v{self.options.get('version') or PIPELINES_API_VERSION}/{subpath}"


if __name__ == "__main__":
    API_KEY = "your_api_key"
    a = PipelineClient(api_key=API_KEY)
    print(json.dumps(a.get_pipelines()))

Advanced oauth2 token storage - thanks @sangaline!

This is an example of how you can use the oauth2_token_getter and oauth2_token_setter kwargs on the client to use custom storage (in this case redis) so that multiple clients can share the same access/refresh tokens generated by the oauth2 requests.

import aioredis
from hubspot3 import Hubspot3


redis_client = await aioredis.create_redis_pool(url, db=db, encoding='utf-8', timeout=10)

def oauth2_token_getter(token_type: str, client_id: str) -> str:
    loop = asyncio.get_event_loop()
    key = f'hubspot-oauth2-tokens:{token_type}:{client_id}'
    return loop.run_until_complete(redis_client.get(key))

def oauth2_token_setter(token_type: str, client_id: str, token: str) -> None:
    loop = asyncio.get_event_loop()
    key = f'hubspot-oauth2-tokens:{token_type}:{client_id}'
    # Token expiration is six hours, so match that when we store the tokens.
    # See: https://developers.hubspot.com/docs/methods/oauth2/refresh-access-token
    expire_in_seconds = 6 * 60 * 60
    loop.run_until_complete(redis_client.set(key, token, expire=expire_in_seconds))

# This client will share oauth2 credentials with other clients configured in the same way.
hubspot3_client = Hubspot3(
    access_token=access_token,
    client_id=client_id,
    client_secret=client_secret,
    refresh_token=refresh_token,
    oauth2_token_getter=oauth2_token_getter,
    oauth2_token_setter=oauth2_token_setter,
)

Testing

I'm currently working on rewriting many of the tests with pytest to work against the public API and ensure that we get the correct type of mock data back. These tests are currently in a very early state - I'll be working soon to get them all built out.

# Install required test packages
pip install pytest pytest-cov
# or
pip install -r requirements-dev.txt

# run tests
make
# or
make test_all

hubspot3's People

Contributors

advance512 avatar antonioirizar avatar artemgordinskiy avatar benaduggan avatar clivecorbishley avatar cvoege avatar danlessa avatar davidemf avatar dpgaspar avatar fredrikwendt avatar guysoft avatar himynameistimli avatar hkage avatar jeking3 avatar jisson avatar jpetrucciani avatar jsancho-gpl avatar kantimati avatar kwbauson avatar maxmorlocke avatar myles avatar ndewani avatar rheinwerk-mp avatar robkorv avatar sangaline avatar selinfildis avatar singingwolfboy avatar vivithemage avatar w1ldpo1nter avatar willhxt avatar

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  avatar  avatar  avatar  avatar

hubspot3's Issues

ACCESS TOKEN example

Now that the use of API KEYS have been depreciated by HS, would you kindly add an example of authenticating using an ACCESS TOKEN to your readme.

Thanks for the recent updates.

Is there OAuth 2 support?

Hey, it's me again :)

Does the library support running an API that requires OAuth 2 authentication?

EDIT:
I also mean if for example I took care of the authentication myself, can I use the access token instead of API key to run the commands using this library?

Thanks!

Retrieving additional properties on deals

Hi, I noticed there isn't an obvious way (to me at least) to conveniently request additional properties when requesting bulk deals. I came accross this using the DealsClient.get_all() method. For now, I am getting around this by building a query string manually and passing it to the method as query=&property=property1.

It seems to me that there should be some way of expanding the requested properties in the method. I could play around with adding this functionality when I have a bit more time.

Of course, if there is a way to do this that I'm completely missing, I'd love to hear about it.

Getting 500 internal server errors at random

Seems to have happened since hubspot had their outage
reproduce:

 engagements_client = EngagementsClient(api_key=api)

 # Handle offset
 print("Getting list of all engagements")

 engagements = engagements_client.get_all()

output (I checked, that status is indeed 500):

Getting list of all engagements
Traceback (most recent call last):
 File "/opt/shapedo/sales/hubspot/get_all_data.py", line 36, in 
 get_all_engagements(os.path.join(args.folder, 'hubspot_engagements.json'), API)
 File "/opt/shapedo/sales/hubspot/engagements.py", line 12, in get_all_engagements
 engagements = engagements_client.get_all()
 File "/usr/local/lib/python3.6/dist-packages/hubspot3/engagements.py", line 73, in get_all
 **options
 File "/usr/local/lib/python3.6/dist-packages/hubspot3/base.py", line 361, in _call
 **options
 File "/usr/local/lib/python3.6/dist-packages/hubspot3/base.py", line 261, in _call_raw
 result = self._execute_request_raw(connection, request_info)
 File "/usr/local/lib/python3.6/dist-packages/hubspot3/base.py", line 182, in _execute_request_raw
 raise HubspotServerError(result, request)
hubspot3.error.HubspotServerError: 
internal error

---- request ----
GET api.hubapi.com/engagements/v1/engagements/paged?limit=250&offset=655163296&hapikey=xxxxxx, [timeout= ]

---- body ----

---- headers ----

---- result ----

---- body -----
{"status":"error","message":"internal error","correlationId":"9df8fff4-739f-4e3a-bb2e-b8e539ddf136","requestId":"c69f7fd841bf5bcc064aebbeab45b65f"}

---- headers -----

---- reason ----
Internal Server Error

---- trigger error ----

using hubspot3==3.2.3

Trouble using the command line "AttributeError: 'int' object has no attribute 'value'"

I am trying to use hubspot3 with this command:

hubspot3 --config config.json crm_associations create --from_object 7307801 --to_object 1181162112 --definition 4

and receive the following error.
AttributeError: 'int' object has no attribute 'value'

I cannot figure it out. Any ideas?

My apologies if this question is super basic and obvious. I'm new to this and after 12 hours I think I need some help.

Emit metrics about API calls

Would you accept a PR that allowed users to pass a callback function or object to the Hubspot3 class allowing it to publish metrics? For example a simple counter of HTTP requests and response codes? Something along the lines of

# hubspot3.metrics or somewhere similar
class MetricsClient(Protocol):
    def counter(self, name: str, value: int, tags: Optional[dict[str, str | int | float]]):
        pass

# hubspot3.base
class BaseClient:
    def __init__(..., metrics_client: MetricsClient = None):
        self.metrics_client = metrics_client

    def _counter(self, name: str, value: int, tags: Optional[dict[str, str | int | float]]):
        if not self.metrics_client:
            return
        try:
            self.metrics_client.counter(name, value, tags)
        except Exception:
            logging.exception("something went wrong publishing a metric")

    def _execute_raw_request(self, conn, request):
        ...
        self._counter("endpoint", 1, {"http_status": 123})
        return result


# user code
class MyMetricsClient:
    def counter(self, name: str, value: int, tags: Optional[dict[str, str | int | float]]):
        # user implemented method to publish the metric somewhere

client = Hubspot3(access_token="blah", metrics_client=MyMetricsClient)

My motivation is using the new Private Apps there is no equivalent to /integrations/v1/limit/daily that shows your API usage. I've raised a feature request with HubSpot but I'm not hopeful.

How would you paginate the contacts?

I am getting the contracts all at once by using
python

hb_client = Hubspot3(api_key=API_KEY)

contacts = hb_client.contacts.get_all()

but I want to be able to paginate this request instead of loading it all in at once. Perhaps 20 at a time. there's no documentation that goes over this.

How to paginate contacts?

When I get the contact list from hub sport it gets them in a large dataset since the project I'm working on has a lot of contacts like over 2000. How do I paginate this?

@api.route("/hubspot/contacts/")
@cross_origin(supports_credentials=True)
def getHubspotContacts():
    """Gets a list of the clients we have in hubspot"""
    API_KEY = "api-key"
    hb_client = Hubspot3(api_key=API_KEY)

    contacts = hb_client.contacts.get_all()
    # display = contacts[range_val:(range_val+20)]
    return jsonify(contacts)

Async implementation is in the roadmap?

Hello!

First of all: this is a really good library and I appreciate the awesome work.

When I have to do hundreds of requests things start to become a bit slowly - which is completely normal - so I was wondering if you guys have any plan to implement an asynchronous programming here.

Otherwise, could you facilitate my (likely) future contribution by pointing out which parts of the code should I look at first?

Thank you very much!

Proposal to improve support for oauth2 authentication with multiple clients

Introduction

Hi @jpetrucciani, I've submitted a small handful of PRs in the past and I'm a regular user/advocate of the project. The company I work at would like to use the library in a distributed environment with oauth2 authentication, but we've run into contention issues where the different containers essentially fight over access tokens because only one can be active at any given time. This leads to almost every request initially failing with 401 errors and then only working after a new access token has been requested from Hubspot. It would be ideal if we could share the tokens across workers through an in-memory store like Redis to avoid these issues. This is a significant enough change that I thought it would be good to open an issue to discuss the approach before implementing it (and I would be happy to implement it, this isn't a request for you to do so).

How oauth2 is currently handled

The easiest way to initialize the client is through the Hubspot3 class in __init__.py. This supports four options which are all required for Hubspot's oauth authentication scheme:

  • client_id - The client ID of the app that's configured for oauth2 authentication. This is non-sensitive and doesn't change.
  • client_secret - The client secret for the app. This is sensitive and doesn't change.
  • access_token - This is a temporary token that provides access to the API as an alternative to using the API key. This is sensitive and expires after 6 hours.
  • refresh_token - This is a temporary token that is required to generate new access tokens after they have expired. This is sensitive, and although it doesn't explicitly expire, it will rotate regularly through the course of requesting new access tokens.

These are stored in a dictionary called auth on a Hubspot3 instance after initialization, and the arguments are passed into the different client classes when they're accessed as properties. For example, accessing the contacts property on the client will initialize a new ContactsClient instance via this code:

    @property
    def contacts(self):
        """returns a hubspot3 contacts client"""
        from hubspot3.contacts import ContactsClient

        return ContactsClient(**self.auth, **self.options)

All of the various clients inherit from BaseClient which stores the credentials on instance properties when initialized. The credentials are then used in BaseClient._call_raw() whenever a request is made, and a new access token is requested if a HubspotUnauthorized exception is raised. The access_token and refresh_token properties are then replaced on the instance by this code:

                        client = OAuth2Client(**self.options)
                        refresh_result = client.refresh_tokens(
                            client_id=self.client_id,
                            client_secret=self.client_secret,
                            refresh_token=self.refresh_token,
                        )
                        self.access_token = refresh_result["access_token"]
                        self.refresh_token = refresh_result["refresh_token"]

The OAuth2Client class handles refreshing the tokens, but it isn't responsible for storage or maintenance of the tokens.

Proposal for supporting dynamic storage of tokens

A relatively simple and backwards compatible change is adding two new initialization parameters to the Hubspot3 and BaseClient classes:

  • oauth2_token_setter - A setter with type typing.Callable([BaseClient, typing.Literal['access_token', 'refresh_token'], str, str), a signature of callable(base_client, token_type, client_id, token), and a default value of lambda self, token_type, client_id, token: setattr(self, token_type, token). This would be used to store either of the tokens through either some external mechanism or just on the instance. The client ID is included to accommodate for the possibility of multiple clients being used in the same application.

  • oauth2_token_getter - A getter with type typing.Callable([BaseClient, typing.Literal['access_token', 'refresh_token'], str), a signature of callable(token_type, client_id), and a default value of lambda self, token_type, client_id: getattr(self, token_type). This would be used to retrieve either of the tokens through some external mechanism.

The logic in the __init__() methods of BaseClient and Hubspot3 as well as the BaseClient._call_raw() method would then use the setter and getter in place of direct attribute access. The access_token/refresh_token attributes would still be used by default, and this should be completely backward compatible while also supporting more sophisticated storage mechanisms.

Conclusion

Apologies for the verbose issue, but I wanted to clearly lay out what I'm proposing. If this sounds good to you, then I'll go ahead and implement it.

Delete company

Hi Jacobi,

Thanks so much for sharing this great project!

I've identified some companies I'd like to delete from Hubspot, and would like to extend the Company client to support it. I found this documentation, and am putting together a PR now :)

Method to debug requests?

Hey,
Is there a way to see what kind of request is send for a call?

The specific reason I am asking is that I ran

deals_client = DealsClient(api_key=API)
deals_client.get_all(propertiesWithHistory="Amount")

I want to see if propertiesWithHistory="Amount" gets appended at all, documented here.

Pulling custom properties for Contacts

Hello,

I believe this is closely related to ticket #14. I'm trying to pull a number of fields across all contacts: vid, email, and a custom field.

Is there a way to select this within .get_all()? Or is there another way to solve for this?

Exceptions should not expose email address

I think we have room for improvements on the exception handling side.

this is one example of output when a server side error happens:

HubspotServerError

Hubspot Error

---- request ----
POST api.hubapi.com/contacts/v1/contact/createOrUpdate/email/[email protected]?, [timeout=<class 'int'>]

---- body ----
<class 'NoneType'>

---- headers ----
<class 'dict'>

---- result ----
<class 'int'>

---- body -----
<html>
<head><title>502 Bad Gateway</title></head>
<body bgcolor="white">
<center><h1>502 Bad Gateway</h1></center>
<hr><center>cloudflare</center>
</body>
</html>


---- headers -----
<class 'http.client.HTTPMessage'>

---- reason ----
Bad Gateway

---- trigger error ----
<class 'NoneType'>

As a solution on such cases we can catch the exception and try to somehow parse the reason, but would be way more convenient if such thing come from the package.

Hubspot object content inconsistencies?

Hi,

After having upgraded to the last version of hubspot3 I find that sometime, when fetching objects from the Hubspot API, i may retrieve dict formatted in different ways regarding objects properties.

Sometimes its the 'hubspot' way of formatting object properties:


{'objectId': 47863442,
 'portalId': 5799819,
 'isDeleted': False,
 'objectType': 'PRODUCT',
'properties': {'name': {'value': "My product name",
   'source': 'CRM_UI',
   'sourceId': None,
   'versions': [{'name': 'name',
     'value': "My product name",
     'source': 'CRM_UI',
     'requestId': 'dcb6af03-4c5e-499a-9448-8c689c4f984a',
     'sourceVid': [],
     'timestamp': 1570095121725}],
   'timestamp': 1570095121725},
  'price': {'value': '115',
   'source': 'CRM_UI',
   'sourceId': None,
   'versions': [{'name': 'price',
     'value': '115',
     'source': 'CRM_UI',
     'requestId': 'dcb6af03-4c5e-499a-9448-8c689c4f984a',
     'sourceVid': [],
     'timestamp': 1570095121725}],
   'timestamp': 1570095121725}
}}

and sometimes I only retrieve key / values (not placed under the properties key):

{
    'id': 28612078,
    'name': 'My product name',
    'price': '115',
}

I think that getting a single product will returns to me an hubspot payload while retrieving all the products will return to me prettified products. It seems to be the same thing for most of clients.

I personally prefer the second format that I find more natural for developers. And it could be great to be able to use this format for both input and outputs.

But what I find disturbing is that not all of the clients methods are returning the object information the same way.

What do you think of that?

Extending Classes

Hi -

I was using the code snippet from guysoft to extend the base client to emails (for which thanks!), and I noticed a couple of things:

  1. One needs to add "**options" to the params dictionary inside the function definition(s) if one wants to pass parameters as arguments to the function. I don't know if I said that right. What I mean is:
def get_email_campaigns(self, **options):
        params = {**options}
  1. I'm not sure how pagination works, really, in the extended classes, but something like this seems to:
def get_email_campaigns(self, **options):
        params = {**options}
        output = []
        finished = False
        while not finished:
            page = self._call("campaigns", method="GET", params=params)
            params['offset'] = page['offset']
            output.extend(page['campaigns'])
            if page['hasMore'] == False:
                finished = True

        return output

Neither of these is issues with the code itself, I don't think, but maybe they'd be useful for the documentation? I'm very much a beginner and I find it helpful to have examples spelled out for me. Thought I'd maybe save someone some time. Because this really is the best python sdk out there for hubspot!

Andrew S.

Need to supply refresh_token or access_token for Forms API

In base.py:

        if not (self.api_key or self.access_token or self.refresh_token):
            raise Exception("Missing required credentials.")

However, obviously Forms API does not need any of these. Instantiating it raises this error. It seems unnecessary, and even risky. (I would not want to send these values to the Forms API.)

get_all() fails

thanks for writing this. trying to use for some quick api prototyping.

when trying the quickstart guide, i get the following error.

(Python 3.8.2)

Screen Shot 2020-05-14 at 11 39 31 AM

Use Requests

I think it would be better to use requests library instead of making your own off of urllib. There is an endpoint for uploading files that needs to be multi-part. Your current library will not support and will consider a re-write. Probably better to just use requests as most of this stuff is built in.

This package is having an issue loading for me. Hubspot3UsageLimits

File "send_docs.py", line 22, in

from hubspot3 import Hubspot3

File "C:\Users\Ty Cooper\AppData\Local\Programs\Python\Python37\lib\site-packages\hubspot3_init_.py", line 9, in

class Hubspot3UsageLimits:

File "C:\Users\Ty Cooper\AppData\Local\Programs\Python\Python37\lib\site-packages\hubspot3_init_.py", line 21, in Hubspot3UsageLimits

collected_at: datetime = datetime.fromtimestamp(0),

OSError: [Errno 22] Invalid argument

Error on calling client.usage_limits

I am getting this error when calling client.usage_limits

---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-4-99b56d0c4e75> in <module>
----> 1 client.usage_limits

~/projects_backup/env/lib/python3.7/site-packages/hubspot3/__init__.py in usage_limits(self)
    369                 current_usage=int(limits["currentUsage"]),
    370                 fetch_status=limits["fetchStatus"],
--> 371                 resets_at=datetime.fromtimestamp(int(limits["resetsAt"]) / 1000),
    372                 usage_limit=limits["usageLimit"],
    373             )

TypeError: int() argument must be a string, a bytes-like object or a number, not 'NoneType'

Updating a Hubspot Company is not doing anything

Hi,

I'm struggling to get the update function in the company client to work.

def update(self, company_id: str, data: Dict = None, **options) -> Dict:
"""update the given company with data"""
data = data or {}
return self._call(
"companies/{}".format(company_id), data=data, method="PUT", **options
)

I'm wondering if its asking for data, but the request in the Hubspot API is looking for properties. Would this be causing it not to work? For example, the sample JSON hubspot is showing in the API, https://legacydocs.hubspot.com/docs/methods/companies/update_company, is expecting properties, not data. The sample JSON given, is:
Example PUT JSON:
{
"properties": [
{
"name": "description",
"value": "A far better description than before"
}
]
}

Right now I'm calling the function with:
updateCompanyDict = {
"name":"rpm_id",
"value":rpmCustomerAccountId
}

response = companies_client_update.update(company_id=company.companyId, data=updateCompanyDict, debug=True)

The debug for the request above shows:
{
"data": "{"name": "rpm_id", "value": 1286}",
"headers": {
"Accept-Encoding": "gzip",
"Content-Type": "application/json"
},
"url": "/companies/v2/companies/4326408039?hapikey=hapikey"
}

where hapikey, is our key.

Can you please shed some light?

Disabling auto-retry spams out warnings

According to the documentation:

# Retrying API Calls

By default, hubspot3 will attempt to retry all API calls up to 2 times
upon failure.

If you'd like to override this behavior, you can add a `number_retries`
keyword argument to any Client constructor, or to individual API calls.

However, setting number_retries=0 causes the library to spam out warnings on the first failure, e.g.

Too many retries for /deals/v1/deal/110248399499?
Too many retries for /deals/v1/deal/102249436582?
Too many retries for /deals/v1/deal/102351008917?

When disabling the auto-retry mechanism it is expected that the library will not auto-retry, and will not output anything in regards to retrying.

Can we disable this warning in case of number_retries==0?

Result window too large when querying recently modified companies

I was experiencing an issue while using the companies client to query for recently modified companies. The client sends the request 250 at a time, per the default limit parameter and then increments the offset by that amount for the next query. Then when it gets to the upper limit of requested records (10000) it continues and sends another request with a 10250 offset, which throws a bad request error. Since the finished boolean value is only ever made to true when batch["hasMore"] is set to false, even if it has reached the upper limit, then it continues to make requests.

I have no idea why my company has over 10000 modified company records or if hubspot is returning more than just the recently modified records, but it seems to me like the client should stop looping at that upper limit to avoid the error. An additional check at the bottom of the loop should be enough to solve this:

finished = not batch["hasMore"] or offset + limit > 10000

I'd also probably move that line to be after the new offset is set, so we can get the value of what it would be for the next call.

It's also a tough one to get around because limit only limits the size of each call in the loop. I would also suggest adding some sort of way to limit the total number of records returned from a call, which isn't a necessity but would help to work around something like this.

I can probably submit a fix for this if you agree on the solution I proposed or if you have any other ideas I'd love to hear them.

Naming Conventions and Deprecation Policy

My colleagues and I are currently in the process of extending existing clients by adding more endpoints (mainly contacts) and adding new clients (e.g. Ecommerce Bridge) - PRs will follow. ;-)

When it comes to the Contacts API, one of the endpoints we want to add is the Update a contact by email endpoint (currently, hubspot3 only offers the corresponding endpoint that works with IDs).
To implement this as a new method, we will have to choose a name for it - which is why we found that the methods of the ContactsClient use multiple different naming conventions:

  • containing the by_id/by_email suffix, e.g. get_contact_by_id
  • containing the a_contact suffix, e.g. update_a_contact
  • short names, e.g. update

The first two conventions seem to follow the names from the endpoint overview on the left side of the HubSpot API docs, although the a_contact suffix is not necessarily nice.

The update_a_contact and update methods also seem to contain the same logic and are therefore duplicates, which means that one of them should probably be removed in the long run.

My questions are:

  • What is the preferred naming scheme for these methods? Should the new method be called update_a_contact_by_email to follow the names in the HubSpot API overview or should it be called update_contact_by_email to follow the convention of the get_...-methods (while renaming the current update method to update_contact_by_id)? Or even something shorter like update_by_email?
  • How to deal with the duplicate update method and other deprecations? I would generally suggest to change one of the duplicate methods to give a deprecation warning and then simply call the other method (that way, the duplicate code is removed, but both methods remain for implementations that already use them; the method containing the deprecation warning can then be removed in the future after developers had the time to react to the warning). The same could be applied to methods that would potentially need renaming after choosing a naming scheme in the first question. Is there a deprecation policy so we can already include the version number in which such methods would finally be removed in such warnings (we could also just say "will be removed in the future")?

We would implement all of this when extending the ContactsClient and throw a PR your way - we just need to know what the preferred naming convention/deprecation policy would be.

Running tests not against the live HubSpot demo account

My colleagues and I a currently working on extending the contacts API (as mentioned also in #39).

The tests for this (and other) API rely on HubSpot's live demo account. This causes some problems. Mainly the tests are failing at the moment because the demo account has reached its daily limit. We need to wait until the next day, when the limit is reset, to run all tests, hoping is has not been exceeded again.

Furthermore the tests can fail when the live database does not contain the expected records. Two examples: When the database is flushed, all tests relying on the contact from the BASE_CONTACT would fail. And if there are less than 20 records in the database, testing the limit parameter in the test test_get_all will not fail but will not test anything useful either since there will always be less than 20 results, regardless of the parameter value.

As a solution we would suggest to mock the API calls and their responses in the current unit tests. We would include the necessary changes (at least for the contact API tests) in the following PR.

Let me know what do you think.

Products API

Hi Jacobi,

First of all, thanks a lot for your work on this package!
My current client is in the process of migrating to Hubspot, and hubspot3 is helping me a lot in connecting it to our Django-backed website :)

I now realize I am going to need to use the Products API.
Do you have plans to add it soon or not?
Please do not feel any pressure to do so; if not, I'll be happy to do it myself and submit a little PR.
I'm merely asking to avoid duplicated efforts on that matter ^^

Cheers!

Not found response on create or update pipeline

use plural pipelines instead of pipeline in the url of create and update methods
according to the description of hubspot

https://api.hubapi.com/crm-pipelines/v1/pipelines/deals?hapikey=demo

    def create(self, object_type, data=None, **options):
        data = data or {}
        return self._call(
            "pipeline/{}".format(object_type), data=data, method="POST", **options
        )

    def update(self, object_type, key, data=None, **options):
        data = data or {}
        return self._call(
            "pipeline/{}/{}".format(object_type, key),
            data=data,
            method="PUT",
            **options
        )

No verbose output to log in case of retries

When using the base client (we extended it for the Hubspot Associations API), we see the following in our logs:

2020-05-13 23:09:51,608 [22/#7f68ac2b2ae0] WARNING in base._call_raw:323: Too many retries for /crm-associations/v1/associations/123456/HUBSPOT_DEFINED/1?
2020-05-13 23:09:52,662 [22/#7f68ac2b2ae0] WARNING in base._call_raw:323: Too many retries for /crm-associations/v1/associations/123456/HUBSPOT_DEFINED/1?
2020-05-13 23:09:53,722 [22/#7f68ac2b2ae0] WARNING in base._call_raw:323: Too many retries for /crm-associations/v1/associations/123456/HUBSPOT_DEFINED/1?

Further in the logs, there no log output of any kind explaining what issue was encountered, what HTTP status code was returned, what error was encountered. No exception is thrown, and eventually (after 2-3 tries) the function using the BaseClient just returns None.

Looking at the code:

            except HubspotError as exception:
                if try_count > num_retries:
                    logging.warning("Too many retries for {}".format(url))
                    raise
                # Don't retry errors from 300 to 499
                if (
                    exception.result
                    and exception.result.status >= 300
                    and exception.result.status < 500
                ):
                    raise
                self._prepare_request_retry(method, url, headers, data)
                self.log.warning(
                    "HubspotError {} calling {}, retrying".format(
                        exception, url
                    )
                )
            # exponential back off - wait 0 seconds, 1 second, 3 seconds, 7 seconds, 15 seconds, etc
            time.sleep((pow(2, try_count - 1) - 1) * self.sleep_multiplier)

it seems like an exception should have indeed been thrown, so I am not certain why I don't see it outside. Anyways, I see the self.log.warning() call and I wonder why this output is not written to the log.

Is there any way to configure hubspot3 to print this warning to log in case of an error causing a retry?

Getting ModuleNotFoundError: No module named 'requests' on Elastic Beanstalk

Using cached https://files.pythonhosted.org/packages/78/d7/3e3a9ff716b29c07b451bcd200449cdf360b15415f9bd17c162e1b33460c/hubspot3-3.2.11.tar.gz
    Complete output from command python setup.py egg_info:
    Traceback (most recent call last):
      File "<string>", line 1, in <module>
      File "/tmp/pip-build-mn7fko9u/hubspot3/setup.py", line 5, in <module>
        from hubspot3.globals import __version__
      File "/tmp/pip-build-mn7fko9u/hubspot3/hubspot3/__init__.py", line 5, in <module>
        from hubspot3.error import HubspotBadConfig, HubspotNoConfig
      File "/tmp/pip-build-mn7fko9u/hubspot3/hubspot3/error.py", line 6, in <module>
        from hubspot3.utils import force_utf8
      File "/tmp/pip-build-mn7fko9u/hubspot3/hubspot3/utils.py", line 4, in <module>
        import requests
    ModuleNotFoundError: No module named 'requests'

Getting this on Elastic Beanstalk, if I pip install requests separately and then run it all works fine?

Unable to pass params to contacts.get_all()

As documented in the project README (https://github.com/jpetrucciani/hubspot3#passing-params), I should pass params to send extra properties to hubspot. Seems this doesn't work for contacts:

contacts = client.contacts.get_all(params={"showListMemberships": "true"}, extra_properties=["hs_language"])

I got:

    contacts = client.contacts.get_all(params={"showListMemberships": "true"}, extra_properties=["hs_language"])
  File "/home/kvdb/.local/share/virtualenvs/send-cMuOhGEa/lib/python3.8/site-packages/hubspot3/contacts.py", line 163, in get_all
    batch = self._call(
TypeError: _call() got multiple values for keyword argument 'params'

Forms API requires URL Encoding

To get the Forms API submission to work, I had to do the following:

        dataToSubmit = dict(data)
        dataToSubmit['hs_context'] = json.dumps(context)

        # Build the request body, URL encoded
        urlEncodedData = urlencode(dataToSubmit, quote_via=quote_plus)

        formsClient.submit_form(
            portal_id=config.hubspot.HUBSPOT_PORTAL_ID,
            form_guid=formGuid,
            data=urlEncodedData,
        )

I think support for URL Encoding the data makes sense here, this should not be required to be done outside of the class itself. Without this, form submission fails.

CLI interface issue

Jacobi,

Firstly, thank you (and other contributors) for this library.

I'm a bit new to python (despite many years as a developer - I'm old ;-) and thought this was a great opportunity to develop my python skills while working on a Hubspot integration project.

My first few attempts to query Hubspot have worked fine. As we are at the design state, I would be expecting to be trialing many different queries and updates, to clarify the approach we'll take to integrations.

I was particularly interested in the CLI approach for this.. Sadly, I haven't had a lot of success so far... and this could easily be a case of my inexperience and environment (I'm working in a WSL environment.. have access to many flavours of linux, but this is by far the most convenient in the workplace).

I believe I've followed the setup instructions correctly (but who isn't blind to a few mistakes)

I think it's the elegance of python-fire that's defeating me at the moment... or maybe understanding the implementation.

The very simple initial call via CLI is as follows:
% hubspot3 --api-key="blahblah" contacts get-all

The innermost exception appears in main.py in _replace_stdin_token, which is executing a return with an uninitialized variable, new_args.

I believe this is because the following 'if' will only initialise new_args if stdin_indices or stdin_keys contain a value based on the value of STDIN_TOKEN

 if stdin_indices or stdin_keys:
        value = json.load(sys.stdin)
        new_args = list(args)
        for index in stdin_indices:
            new_args[index] = value
        for key in stdin_keys:
            kwargs[key] = value

--> return new_args, kwargs
No matter what I've tried in terms of cli calls, I get an exception at the above line (here's the run time exception, I will add debug session below)

File "/home/ben/fishscaler/hubspot3/main.py", line 189, in _replace_stdin_token
return new_args, kwargs
UnboundLocalError: local variable 'new_args' referenced before assignment

Below is a debug session, where I think the only explanation is, both stdin_indices and stdin_keys aren't initialised with a value... and working back from there has defeated me so far.

Any light you can shed on this would be appreciated.

Here's my debug session, including a print(str(sys.argv)) ahead of the call to main()

/home/ben/fishscaler/cli.py(9)()
-> if name == 'main':
(Pdb) n
/home/ben/fishscaler/cli.py(10)()
-> sys.argv[0] = re.sub(r'(-script.pyw?|.exe)?$', '', sys.argv[0])
(Pdb) n
/home/ben/fishscaler/cli.py(11)()
-> print(str(sys.argv))
(Pdb) n
['cli.py', '--api-key=blahblah', 'contacts', 'get-all']
/home/ben/fishscaler/cli.py(12)()
-> sys.exit(main())
(Pdb) s
--Call--
/home/ben/fishscaler/hubspot3/main.py(231)main()
-> def main():
(Pdb) c
/home/ben/fishscaler/hubspot3/main.py(179)_replace_stdin_token()
-> index for index, value in enumerate(args) if value == self.STDIN_TOKEN
(Pdb) n
/home/ben/fishscaler/hubspot3/main.py(181)_replace_stdin_token()
-> stdin_keys = [key for key, value in kwargs.items() if value == self.STDIN_TOKEN]
(Pdb) n
/home/ben/fishscaler/hubspot3/main.py(182)_replace_stdin_token()
-> if stdin_indices or stdin_keys:
(Pdb) p str(stdin_indices)
'[]'
(Pdb) p stdin_indices
'[]'
.... my read on this is - these two empty lists will result in a return of uninitialised new_args

I'm afraid I'm getting lost in the ClientCLIWrapper and HubspotCLIWrapper calls..

Please excuse my lack of experience here... I'd really like to use hubspot3, and contribute if I can.

Thanks,
Ben

Timeline API

Hey,

I've recently discovered this package and I thank you for developing it!

I'd like to ask please if there's a plan on supporting the Timeline API?

Thanks!

Review API and use version 3 instead of 1?

Thanks for providing this wrapper, it works well for us. However, it's been a while since v3 of Hubspot's API was made the default. I just realized /contacts/v1/contact/vid/6191/profile?hapikey=1111111111111 was used, instead of /crm/v3/objects/contacts/6191. While v1 works, it may eventually be dropped.

(I discovered this when trying to figure out how to turn off the properties history/versions. The docs for the current API version (v3) said nothing about it.)

Engagement API, function get_associated contains two print statements

Hi,

Sorry if I misunderstood, but I get some printouts when running and suspect it is these two print statements doing it:

while not finished:
print(offset)
batch = self._call(
"engagements/associated/{}/{}/paged".format(object_type, object_id),
method="GET",
params={"limit": query_limit, "offset": offset},
**options
)
print(len(batch["results"]))

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.