From a6219fd0e0f1e823a41e6ae950f080737738ffdd Mon Sep 17 00:00:00 2001 From: William McVey Date: Thu, 20 Jun 2019 17:12:59 -0400 Subject: [PATCH 1/7] Major rewrite of core API elements --- convertkit/main.py | 196 ++++++++++++++++++++++++++++++++++----------- 1 file changed, 149 insertions(+), 47 deletions(-) diff --git a/convertkit/main.py b/convertkit/main.py index 77697c7..6fcddea 100644 --- a/convertkit/main.py +++ b/convertkit/main.py @@ -1,70 +1,172 @@ import requests from unittest import TestCase +import requests + + class APIError(Exception): pass -class APIModel(object): - def __init__(self, creds, requester, params): - self.creds = creds - self.requester = requester - self.params = params + +class APIModel: + def __init__(self, json_blob, api): + self.log = logging.getLogger("ConvertKit." + self.__class__.__name__) + self.api = api + self.obj = self.decode(json_blob, api) def __getattr__(self, attr): - try: - return self.params[attr] - except: - raise AttributeError + return self.obj[attr] + + @staticmethod + def decode(blob, api): + """A basic decoder that simply returns the blob that is passed in + """ + return blob -class Subscriber(APIModel): def __repr__(self): - return ''.format(self.id) + return f'<{self.__class__.__name__} {" ".join([f"{k}={v!r}" for k,v in self.obj.items()])}>' -class Form(APIModel): - def add_subscriber(self, email, first_name): - resp = self.requester.post( - '{}{}'.format(self.creds.base_url, 'v3/forms/{}/subscribe'.format(self.id)), - data={ - 'email': email, - 'name': first_name, - }, params={'api_key': self.creds.api_key}) - if resp.status_code >= 300: - raise APIError(resp.content) +class SubscriptionMixin: + """A Mixin for object types that support subscriptions/membership - return Subscriber(self.creds, self.requester, resp.json()['subscription']) + Requires a class or instance variable MODEL_ENDPOINT + """ - def __repr__(self): - return ''.format(self.params.get('name', 'Unknown')) + def list_subscriptions(self, sort_order="asc", subscriber_state=None): + if not self.api.api_secret: + raise APIError("Form subscription listing endpoint needs API secret") + factory = lambda response: [Subscription(x, api=self.api) for x in response['subscriptions']] + resp = self.api.GET(f'{self.MODEL_ENDPOINT}/{self.id}/subscriptions', factory=factory, api_secret=self.api.api_secret) + self.log.info(f"{self} subscriptions: {resp}") + return resp -class CredentialsObject(object): - def __init__(self, api_key, base_url="https://api.convertkit.com/"): - self.api_key = api_key - self.base_url = base_url + def add_subscriber(self, email, first_name=None, params=None, **kwargs): + params = dict(params) if params else {} + params.update(kwargs) + if first_name: + params["first_name"] = first_name + resp = self.api.POST(f'{self.MODEL_ENDPOINT}/{self.id}/subscribe', + factory=lambda x: Subscription(x['subscription'], api=self.api), + email=email, params=params) + return resp + + +class Form(APIModel, SubscriptionMixin): + MODEL_ENDPOINT = "/forms" + + def __str__(self): + return f"{self.id} {self.name}{' '+self.title if 'title' in self.obj else ''}" + +class Subscriber(APIModel): + pass + + +class Subscription(APIModel): + @staticmethod + def decode(blob, api): + blob["subscriber"] = Subscriber(blob["subscriber"], api) + return blob + + +class Account(APIModel): + pass + +class Course(APIModel, SubscriptionMixin): + MODEL_ENDPOINT = "/courses" + + +class Tag(APIModel, SubscriptionMixin): + MODEL_ENDPOINT = "/tags" -class Forms(object): - def __init__(self, creds, requester): - self.creds = creds - self.requester = requester - def list(self): - resp = self.requester.get( - '{}{}'.format(self.creds.base_url, 'v3/forms'), - params={ - 'api_key':self.creds.api_key - }) - if resp.status_code >= 300: - raise APIError(resp.content) - return [Form(self.creds, self.requester, x) for x in resp.json()['forms']] class ConvertKit(object): - def __init__(self, api_key, base_url="https://api.convertkit.com/", requester=None): - self.creds = CredentialsObject(api_key, base_url) - self.requester = requester or requests + BASE_URL = "https://api.convertkit.com/v3" - @property - def forms(self): - return Forms(self.creds, self.requester) + def __init__(self, api_key, api_secret=None, requester=None): + self.api_key = api_key + self.api_secret = api_secret + self.requester = requester or requests + self.log = logging.getLogger(self.__class__.__name__) + + def GET(self, endpoint, factory=None, params=None, **kwargs): + """Make a GET request to an API endpoint + """ + params = dict(params) if params is not None else {} + params["api_key"]=self.api_key + params.update(kwargs) + resp = self.requester.get( + ''.join([self.BASE_URL, endpoint]), + params=params) + self.log.debug(f"Response: {resp} status: {resp.status_code} json: {resp.json()}") + if resp.status_code >= 300: + raise APIError(resp.content) + if factory: + return factory(resp.json()) + else: + return resp.json() + + def POST(self, endpoint, factory=None, params=None, **kwargs): + """Make a GET request to an API endpoint + """ + params = dict(params) if params is not None else {} + params["api_key"]=self.api_key + params.update(kwargs) + resp = self.requester.post( + ''.join([self.BASE_URL, endpoint]), + data=params) + self.log.debug(f"Response: {resp} status: {resp.status_code}") + if resp.status_code >= 300: + raise APIError(resp.content) + # import code; code.interact(banner=f"POST> {endpoint} {params}", local=dict(globals(), **locals())) + if factory: + return factory(resp.json()) + else: + return resp.json() + + def list_forms(self): + factory = lambda response: [Form(x, api=self) for x in response['forms']] + resp = self.GET("/forms", factory) + self.log.info(f"list_forms={resp}") + return resp + + def find_form(self, form_id=None, form_name=None): + forms = self.list_forms() + self.log.info(f'find_form ids = {",".join([str(x.id) for x in forms])}') + if form_id is not None: + forms = [f for f in forms if f.id == form_id] + if form_name is not None: + forms = [f for f in forms if f.name == form_name] + if len(forms) == 0: + raise RuntimeError(f"Did not find a form with matching search form_id={form_id} form_name={form_name}") + if len(forms) > 1: + raise RuntimeError(f"More than one form matched search form_id={form_id} form_name={form_name}") + return forms.pop() + + def account(self): + if not self.api_secret: + raise APIError("account endpoint needs API secret") + resp = self.GET("/account", lambda x: Account(**x), api_secret=self.api_secret) + self.log.info(f"account={resp}") + return resp + + def sequences(self): + factory = lambda response: [Course(x, api=self) for x in response['courses']] + resp = self.GET("/courses", factory) + self.log.info(f"sequences={resp}") + return resp + + def tags(self): + factory = lambda response: [Tag(x, api=self) for x in response['tags']] + resp = self.GET("/tags", factory) + self.log.info(f"tags={resp}") + return resp + + def create_tag(self, name, description): + resp = self.POST("/tags", factory=lambda x: Tag(x, api=self), name=name, description=description) + self.log.info(f"create_tag={resp}") + return resp class FormTestCase(TestCase): From 43edb08e655d15687aca4d13950c26e1f6d9ee6c Mon Sep 17 00:00:00 2001 From: William McVey Date: Thu, 20 Jun 2019 17:13:50 -0400 Subject: [PATCH 2/7] More functional main routine CLI --- convertkit/main.py | 67 +++++++++++++++++++++++++++++++++++++++------- 1 file changed, 58 insertions(+), 9 deletions(-) diff --git a/convertkit/main.py b/convertkit/main.py index 6fcddea..8c36a0e 100644 --- a/convertkit/main.py +++ b/convertkit/main.py @@ -1,4 +1,6 @@ -import requests +#!/usr/bin/env python + +import logging from unittest import TestCase import requests @@ -174,15 +176,62 @@ def test_attrs_accessible_like_object(self): f = Form(None, None, {'test': 1}) self.assertEqual(f.test, 1) + if __name__ == '__main__': import os, sys from pprint import pprint - - key = os.getenv('CONVERTKIT_API_KEY') - if not key: - print("You must specify the CONVERTKIT_API_KEY environment variable") + import argparse + import yaml + + cli = argparse.ArgumentParser() + cli.add_argument("-C", dest="credentials", action="store", default="creds.yaml", + type=lambda x: yaml.safe_load(open(x)), + help="Credentials config file (default: %(default)s)") + cli.add_argument("-v", "--verbose", action="store_true", help="Provide verbose informative messages") + cli.add_argument("-d", "--debug", action="store_true", help=argparse.SUPPRESS) + cli.add_argument("--form-id", type=int, action="store", help="form identifier to operate against") + cli.add_argument("--subscriber", nargs=2, metavar="EMAIL FIRST_NAME", action="store", + help="subscribe an individual to a form or tag") + cli.add_argument("command", action="store", help="Command to execute", + # really should generate with inspection + choices=["list_forms", "account", "sequences", "tags", "list-subscriptions", "subscribe"]) + args = cli.parse_args() + + if args.debug: + loglevel = logging.DEBUG + elif args.verbose: + loglevel = logging.INFO + else: + loglevel = logging.WARN + logging.basicConfig(level=loglevel) + log = logging.getLogger("ConvertKit.cli") + + key = args.credentials['api_key'] + secret = args.credentials['api_secret'] + + + ck = ConvertKit(key, api_secret=secret) + + if args.form_id is not None: + form = ck.find_form(form_id=args.form_id) + print(form) + if args.command == "list-subscriptions": + pprint(form.list_subscriptions()) + if args.command == "subscribe": + if not args.subscriber: + log.error("You must specify a subscriber with --subscribe") + sys.exit(1) + email, name = args.subscriber + subscription = form.add_subscriber(email, name) + print(subscription) + sys.exit(0) + + method = getattr(ck, args.command) + if not method: + log.error(f"Couldn't find execution method for API endpoint {args.command}") sys.exit(1) - - ck = ConvertKit(key) - forms = ck.forms.list() - pprint([(x.id, x.name) for x in forms]) + results = method() + try: + print("\n".join(map(str, results))) + except TypeError: + pprint(results) From f2e23c8a4630031b88e4d1d06649d8da78551c08 Mon Sep 17 00:00:00 2001 From: William McVey Date: Thu, 20 Jun 2019 17:16:20 -0400 Subject: [PATCH 3/7] A sample creds.yaml file for the CLI --- convertkit/creds.yaml.SAMPLE | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 convertkit/creds.yaml.SAMPLE diff --git a/convertkit/creds.yaml.SAMPLE b/convertkit/creds.yaml.SAMPLE new file mode 100644 index 0000000..275335e --- /dev/null +++ b/convertkit/creds.yaml.SAMPLE @@ -0,0 +1,4 @@ +# Info is at: https://app.convertkit.com/account/edit + +api_key: YOUR_KEY +api_secret: YOUR_SECRET From cc940707f37ec89b3558beb80647993d09989aac Mon Sep 17 00:00:00 2001 From: William McVey Date: Wed, 18 Sep 2019 11:05:23 -0400 Subject: [PATCH 4/7] Added handling of paginated returns. Added find_tag() and CLI for subscriber listing by tag --- convertkit/main.py | 61 +++++++++++++++++++++++++++++++++++++++------- 1 file changed, 52 insertions(+), 9 deletions(-) diff --git a/convertkit/main.py b/convertkit/main.py index 8c36a0e..6b97efe 100644 --- a/convertkit/main.py +++ b/convertkit/main.py @@ -38,7 +38,7 @@ def list_subscriptions(self, sort_order="asc", subscriber_state=None): if not self.api.api_secret: raise APIError("Form subscription listing endpoint needs API secret") factory = lambda response: [Subscription(x, api=self.api) for x in response['subscriptions']] - resp = self.api.GET(f'{self.MODEL_ENDPOINT}/{self.id}/subscriptions', factory=factory, api_secret=self.api.api_secret) + resp = self.api.GET(f'{self.MODEL_ENDPOINT}/{self.id}/subscriptions', field="subscriptions", factory=factory, api_secret=self.api.api_secret) self.log.info(f"{self} subscriptions: {resp}") return resp @@ -92,11 +92,12 @@ def __init__(self, api_key, api_secret=None, requester=None): self.requester = requester or requests self.log = logging.getLogger(self.__class__.__name__) - def GET(self, endpoint, factory=None, params=None, **kwargs): + def GET(self, endpoint, field=None, factory=None, params=None, page=1, **kwargs): """Make a GET request to an API endpoint """ params = dict(params) if params is not None else {} params["api_key"]=self.api_key + params["page"]=page params.update(kwargs) resp = self.requester.get( ''.join([self.BASE_URL, endpoint]), @@ -104,13 +105,22 @@ def GET(self, endpoint, factory=None, params=None, **kwargs): self.log.debug(f"Response: {resp} status: {resp.status_code} json: {resp.json()}") if resp.status_code >= 300: raise APIError(resp.content) + response = resp.json() + objects = response.get(field) if field else [] + if response.get("page", 1) != response.get("total_pages", 1): + self.log.info("Found %d pages, requesting next page (%s)", response["total_pages"], page+1) + objects = objects + self.GET(endpoint, field=field, factory=factory, params=params, page=page+1, **kwargs) + if page != 1: + # defer all factory conversions from pagination iteration + return objects + response[field] = objects if factory: - return factory(resp.json()) + return factory(response) else: - return resp.json() + return response def POST(self, endpoint, factory=None, params=None, **kwargs): - """Make a GET request to an API endpoint + """Make a POST request to an API endpoint """ params = dict(params) if params is not None else {} params["api_key"]=self.api_key @@ -129,7 +139,7 @@ def POST(self, endpoint, factory=None, params=None, **kwargs): def list_forms(self): factory = lambda response: [Form(x, api=self) for x in response['forms']] - resp = self.GET("/forms", factory) + resp = self.GET("/forms", field='forms', factory=factory) self.log.info(f"list_forms={resp}") return resp @@ -155,27 +165,51 @@ def account(self): def sequences(self): factory = lambda response: [Course(x, api=self) for x in response['courses']] - resp = self.GET("/courses", factory) + resp = self.GET("/courses", field="courses", factory=factory) self.log.info(f"sequences={resp}") return resp def tags(self): factory = lambda response: [Tag(x, api=self) for x in response['tags']] - resp = self.GET("/tags", factory) + resp = self.GET("/tags", field="tags", factory=factory) self.log.info(f"tags={resp}") return resp + def find_tag(self, id=None, name=None): + """Searches through the tags and returns the first one matching + either the id or name specified or returns None + """ + for tag in self.tags(): + if tag.id == id or tag.name == name: + return tag + return None + def create_tag(self, name, description): resp = self.POST("/tags", factory=lambda x: Tag(x, api=self), name=name, description=description) self.log.info(f"create_tag={resp}") return resp + class FormTestCase(TestCase): def test_attrs_accessible_like_object(self): f = Form(None, None, {'test': 1}) self.assertEqual(f.test, 1) +def output(objects, field=None): + log=logging.getLogger("ObjectGenerator") + for obj in objects: + if field in ("all", None): + print(obj) + else: + try: + print(getattr(obj, field)) + except KeyError: + try: + print(getattr(obj.subscriber, field)) + except: + log.warn("Couldn't extract %s from %r" %(field, obj)) + if __name__ == '__main__': import os, sys @@ -190,6 +224,9 @@ def test_attrs_accessible_like_object(self): cli.add_argument("-v", "--verbose", action="store_true", help="Provide verbose informative messages") cli.add_argument("-d", "--debug", action="store_true", help=argparse.SUPPRESS) cli.add_argument("--form-id", type=int, action="store", help="form identifier to operate against") + cli.add_argument("--tag-id", type=int, action="store", help="tag identifier to operate against") + cli.add_argument("--tag-name", action="store", help="tag name to operate against") + cli.add_argument("--output-fields", choices=["email_address", "id", "all"], action="store", default="all", help="output to show") cli.add_argument("--subscriber", nargs=2, metavar="EMAIL FIRST_NAME", action="store", help="subscribe an individual to a form or tag") cli.add_argument("command", action="store", help="Command to execute", @@ -216,7 +253,7 @@ def test_attrs_accessible_like_object(self): form = ck.find_form(form_id=args.form_id) print(form) if args.command == "list-subscriptions": - pprint(form.list_subscriptions()) + output(form.list_subscriptions(), args.output_fields) if args.command == "subscribe": if not args.subscriber: log.error("You must specify a subscriber with --subscribe") @@ -226,6 +263,12 @@ def test_attrs_accessible_like_object(self): print(subscription) sys.exit(0) + if args.tag_id or args.tag_name: + tag = ck.find_tag(id=args.tag_id, name=args.tag_name) + if args.command == "list-subscriptions": + output(tag.list_subscriptions(), args.output_fields) + sys.exit(0) + method = getattr(ck, args.command) if not method: log.error(f"Couldn't find execution method for API endpoint {args.command}") From f18581d5cadb38a8fe138691351ebb37ee358049 Mon Sep 17 00:00:00 2001 From: William McVey Date: Fri, 31 Jan 2020 17:07:24 -0500 Subject: [PATCH 5/7] Added find_sequence() method to pull stats for sequences. Also added lazy loading to pull data from first page of paginated results. Convienience method to instatiate a client from a config --- convertkit/main.py | 54 +++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 51 insertions(+), 3 deletions(-) diff --git a/convertkit/main.py b/convertkit/main.py index 6b97efe..9ef4813 100644 --- a/convertkit/main.py +++ b/convertkit/main.py @@ -5,6 +5,11 @@ import requests +try: + import yaml +except ImportError: + yaml = None + class APIError(Exception): pass @@ -28,12 +33,20 @@ def decode(blob, api): def __repr__(self): return f'<{self.__class__.__name__} {" ".join([f"{k}={v!r}" for k,v in self.obj.items()])}>' + class SubscriptionMixin: """A Mixin for object types that support subscriptions/membership Requires a class or instance variable MODEL_ENDPOINT """ + @property + def total_subscriptions(self): + """Return how many people are subscribed, without needing to iterate through everyone + """ + return self.obj.get("total_subscriptions") + + def list_subscriptions(self, sort_order="asc", subscriber_state=None): if not self.api.api_secret: raise APIError("Form subscription listing endpoint needs API secret") @@ -82,18 +95,33 @@ class Tag(APIModel, SubscriptionMixin): - class ConvertKit(object): BASE_URL = "https://api.convertkit.com/v3" + @classmethod + def from_yaml_config(cls, filename): + if yaml is None: + raise RuntimeError("No YAML library. Can't instantiate client from_config()") + config = yaml.safe_load(open(filename)) + key = config['api_key'] + secret = config['api_secret'] + return cls(key, api_secret=secret) + def __init__(self, api_key, api_secret=None, requester=None): self.api_key = api_key self.api_secret = api_secret self.requester = requester or requests self.log = logging.getLogger(self.__class__.__name__) - def GET(self, endpoint, field=None, factory=None, params=None, page=1, **kwargs): + def GET(self, endpoint, field=None, factory=None, params=None, page=1, lazy=False, **kwargs): """Make a GET request to an API endpoint + + endpoint: API endpoint + field: object contents extracted from this location of the JSON return object + factory: factory function to return object representation of specified field contents + params: query params + page: pagination page we're fetching + lazy: if True, don't do pagination """ params = dict(params) if params is not None else {} params["api_key"]=self.api_key @@ -107,7 +135,7 @@ def GET(self, endpoint, field=None, factory=None, params=None, page=1, **kwargs) raise APIError(resp.content) response = resp.json() objects = response.get(field) if field else [] - if response.get("page", 1) != response.get("total_pages", 1): + if not lazy and response.get("page", 1) != response.get("total_pages", 1): self.log.info("Found %d pages, requesting next page (%s)", response["total_pages"], page+1) objects = objects + self.GET(endpoint, field=field, factory=factory, params=params, page=page+1, **kwargs) if page != 1: @@ -169,6 +197,20 @@ def sequences(self): self.log.info(f"sequences={resp}") return resp + def find_sequence(self, id=None, name=None, lazy=False): + """Pulls stats for a Sequence by name or number + + If lazy is True, only pull data from first page of sequence, don't iterate through pagination results + """ + if not self.api_secret: + raise APIError("account endpoint needs API secret") + if name is not None: + raise NotImplemented("finding a sequence by name not currently supported") + factory = lambda response: Course(response, api=self) + resp = self.GET(f"/sequences/{id}/subscriptions", factory=factory, api_secret=self.api_secret, lazy=lazy) + return resp + + def tags(self): factory = lambda response: [Tag(x, api=self) for x in response['tags']] resp = self.GET("/tags", field="tags", factory=factory) @@ -225,6 +267,7 @@ def output(objects, field=None): cli.add_argument("-d", "--debug", action="store_true", help=argparse.SUPPRESS) cli.add_argument("--form-id", type=int, action="store", help="form identifier to operate against") cli.add_argument("--tag-id", type=int, action="store", help="tag identifier to operate against") + cli.add_argument("--sequence-id", type=int, action="store", help="sequence identifier to operate against") cli.add_argument("--tag-name", action="store", help="tag name to operate against") cli.add_argument("--output-fields", choices=["email_address", "id", "all"], action="store", default="all", help="output to show") cli.add_argument("--subscriber", nargs=2, metavar="EMAIL FIRST_NAME", action="store", @@ -269,6 +312,11 @@ def output(objects, field=None): output(tag.list_subscriptions(), args.output_fields) sys.exit(0) + if args.sequence_id is not None: + sequence = ck.find_sequence(id=args.sequence_id) + print(sequence) + sys.exit(0) + method = getattr(ck, args.command) if not method: log.error(f"Couldn't find execution method for API endpoint {args.command}") From 9a379202a29d13d84279d846ef9d0ac34adcbf79 Mon Sep 17 00:00:00 2001 From: William McVey Date: Fri, 31 Jan 2020 23:13:35 -0500 Subject: [PATCH 6/7] Bump version --- README.md | 10 +++++----- setup.py | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index fccaaac..d31ddb2 100644 --- a/README.md +++ b/README.md @@ -8,11 +8,11 @@ This is a Python API which aims to implement v3 of the - [X] List Forms - [X] Add subscriber to form -- [ ] Get Courses for account -- [ ] Add subscribers to course -- [ ] Get tags for an account -- [ ] Add subscriber to a tag +- [X] Get Courses for account +- [X] Add subscribers to course +- [X] Get tags for an account +- [X] Add subscriber to a tag - [ ] Subscribing to multiple tags/forms/courses - [ ] Unsubscribe -- [ ] Subscriber list +- [X] Subscriber list - [ ] Update Subscriber diff --git a/setup.py b/setup.py index c151a51..5b175c1 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setup( name='convertkit', - version='0.1', + version='0.2', description='API Client for ConvertKit v3', long_description=readme, license='BSD', From 0957e8dc06e86b0b1dc805e9eaa4415ed89a0e9e Mon Sep 17 00:00:00 2001 From: William McVey Date: Tue, 11 Feb 2020 11:56:17 -0500 Subject: [PATCH 7/7] Bump to 2.2.0. Added Total subscriber interface --- Changelog.txt | 17 +++++++++++++++++ convertkit/main.py | 22 ++++++++++++++++++++-- setup.py | 2 +- 3 files changed, 38 insertions(+), 3 deletions(-) create mode 100644 Changelog.txt diff --git a/Changelog.txt b/Changelog.txt new file mode 100644 index 0000000..49fd874 --- /dev/null +++ b/Changelog.txt @@ -0,0 +1,17 @@ +2.2.0 +====== + * New routine for pulling total subscriber list and counts + + +2.1.0 +====== + * Handle paginated results (also added lazy loading) + * New routine for operations against Sequences + + +2.0.0 +====== + * rewrite of the core API elements from prior version maintained by github + user justinabrahms + * Added CLI interface + diff --git a/convertkit/main.py b/convertkit/main.py index 9ef4813..a5b11d0 100644 --- a/convertkit/main.py +++ b/convertkit/main.py @@ -34,6 +34,9 @@ def __repr__(self): return f'<{self.__class__.__name__} {" ".join([f"{k}={v!r}" for k,v in self.obj.items()])}>' +class FullSubscriberList(APIModel): + pass + class SubscriptionMixin: """A Mixin for object types that support subscriptions/membership @@ -210,6 +213,15 @@ def find_sequence(self, id=None, name=None, lazy=False): resp = self.GET(f"/sequences/{id}/subscriptions", factory=factory, api_secret=self.api_secret, lazy=lazy) return resp + def subscribers(self, lazy=False): + """Look at total registered subscribers + """ + if not self.api_secret: + raise APIError("account endpoint needs API secret") + factory = lambda response: FullSubscriberList(response, api=self) + resp = self.GET(f"/subscribers/", factory=factory, field="subscribers", api_secret=self.api_secret, lazy=lazy) + return resp + def tags(self): factory = lambda response: [Tag(x, api=self) for x in response['tags']] @@ -274,7 +286,7 @@ def output(objects, field=None): help="subscribe an individual to a form or tag") cli.add_argument("command", action="store", help="Command to execute", # really should generate with inspection - choices=["list_forms", "account", "sequences", "tags", "list-subscriptions", "subscribe"]) + choices=["list_forms", "account", "sequences", "tags", "list-subscriptions", "subscribe", "subscriber-count"]) args = cli.parse_args() if args.debug: @@ -317,7 +329,13 @@ def output(objects, field=None): print(sequence) sys.exit(0) - method = getattr(ck, args.command) + if args.command == "subscriber-count": + full_subscribers = ck.subscribers(lazy=True) + print(full_subscribers.total_subscribers) + sys.exit(0) + + + method = getattr(ck, "_".join(args.command.split("-"))) if not method: log.error(f"Couldn't find execution method for API endpoint {args.command}") sys.exit(1) diff --git a/setup.py b/setup.py index 5b175c1..1fbeb31 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setup( name='convertkit', - version='0.2', + version='2.2.0', description='API Client for ConvertKit v3', long_description=readme, license='BSD',