diff --git a/.github/workflows/pythonpublish.yml b/.github/workflows/pythonpublish.yml new file mode 100644 index 0000000..b143a53 --- /dev/null +++ b/.github/workflows/pythonpublish.yml @@ -0,0 +1,26 @@ +name: Upload Python Package + +on: + release: + types: [created] + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Set up Python + uses: actions/setup-python@v1 + with: + python-version: '3.x' + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install setuptools wheel twine + - name: Build and publish + env: + TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} + TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} + run: | + python setup.py sdist bdist_wheel + twine upload dist/* diff --git a/LICENSE b/LICENSE index e561eb0..75560dc 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2017 gophish +Copyright (c) 2020 gophish Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 4a0ca2e..e19d735 100644 --- a/README.md +++ b/README.md @@ -29,4 +29,4 @@ Now you're ready to start using the API! ## Full Documentation -You can find the full Python client documentation [here.](https://gophish.gitbooks.io/python-api-client/content/) +You can find the full Python client documentation [here.](https://docs.getgophish.com/python-api-client/) diff --git a/gophish/api/api.py b/gophish/api/api.py index 20bb28d..3dbea14 100644 --- a/gophish/api/api.py +++ b/gophish/api/api.py @@ -1,13 +1,11 @@ -import requests - from gophish.models import Error - ''' -api.py +api.py Base API endpoint class that abstracts basic CRUD operations. ''' + class APIEndpoint(object): """ Represents an API endpoint for Gophish, containing common patterns @@ -25,30 +23,73 @@ def __init__(self, api, endpoint=None, cls=None): self.endpoint = endpoint self._cls = cls - def get(self, resource_id=None): - """ Gets the details for one or more resources by ID - - Args: - cls - gophish.models.Model - The resource class - resource_id - str - The endpoint (URL path) for the resource + def _build_url(self, *parts): + """Builds a path to an API resource by joining the individual parts + with a slash (/). + + This is used instead of urljoin since we're given relative URL parts + which need to be chained together. Returns: - One or more instances of cls parsed from the returned JSON + str -- The parts joined with a slash """ + return '/'.join(str(part).rstrip('/') for part in parts) + + def request(self, + method, + body=None, + resource_id=None, + resource_action=None, + resource_cls=None, + single_resource=False): + endpoint = self.endpoint + if not resource_cls: + resource_cls = self._cls + if resource_id: - endpoint = '{}/{}'.format(endpoint, resource_id) + endpoint = self._build_url(endpoint, resource_id) + + if resource_action: + endpoint = self._build_url(endpoint, resource_action) - response = self.api.execute("GET", endpoint) + response = self.api.execute(method, endpoint, json=body) if not response.ok: - return Error.parse(response.json()) + raise Error.parse(response.json()) - if resource_id: - return self._cls.parse(response.json()) + if resource_id or single_resource: + return resource_cls.parse(response.json()) - return [self._cls.parse(resource) for resource in response.json()] + return [resource_cls.parse(resource) for resource in response.json()] + + def get(self, + resource_id=None, + resource_action=None, + resource_cls=None, + single_resource=False): + """ Gets the details for one or more resources by ID + + Args: + cls - gophish.models.Model - The resource class + resource_id - str - The endpoint (URL path) for the resource + resource_action - str - An action to perform on the resource + resource_cls - cls - A class to use for parsing, if different than + the base resource + single_resource - bool - An override to tell Gophish that even + though we aren't requesting a single resource, we expect a + single response object + + Returns: + One or more instances of cls parsed from the returned JSON + """ + + return self.request("GET", + resource_id=resource_id, + resource_action=resource_action, + resource_cls=resource_cls, + single_resource=single_resource) def post(self, resource): """ Creates a new instance of the resource. @@ -57,10 +98,12 @@ def post(self, resource): resource - gophish.models.Model - The resource instance """ - response = self.api.execute("POST", self.endpoint, json=(resource.as_dict())) - + response = self.api.execute("POST", + self.endpoint, + json=(resource.as_dict())) + if not response.ok: - return Error.parse(response.json()) + raise Error.parse(response.json()) return self._cls.parse(response.json()) @@ -70,16 +113,16 @@ def put(self, resource): Args: resource - gophish.models.Model - The resource instance """ - + endpoint = self.endpoint if resource.id: - endpoint = '{}/{}'.format(endpoint, resource.id) + endpoint = self._build_url(endpoint, resource.id) - response = self.api.execute("PUT", endpoint, json=resource.as_json()) + response = self.api.execute("PUT", endpoint, json=resource.as_dict()) - if not respose.ok: - return Error.parse(response.json()) + if not response.ok: + raise Error.parse(response.json()) return self._cls.parse(response.json()) @@ -95,6 +138,6 @@ def delete(self, resource_id): response = self.api.execute("DELETE", endpoint) if not response.ok: - return Error.parse(response.json()) + raise Error.parse(response.json()) return self._cls.parse(response.json()) diff --git a/gophish/api/campaigns.py b/gophish/api/campaigns.py index d928d4c..3827ec8 100644 --- a/gophish/api/campaigns.py +++ b/gophish/api/campaigns.py @@ -1,11 +1,10 @@ -import json - -from gophish.models import Campaign, Error +from gophish.models import (Campaign, CampaignSummary, CampaignSummaries, + CampaignResults) from gophish.api import APIEndpoint class API(APIEndpoint): - def __init__(self, api, endpoint='/api/campaigns/'): + def __init__(self, api, endpoint='api/campaigns/'): """ Creates a new instance of the campaigns API """ super(API, self).__init__(api, endpoint=endpoint, cls=Campaign) @@ -30,10 +29,30 @@ def delete(self, campaign_id): return super(API, self).delete(campaign_id) - def summary(campaign_id=None): - """ Returns the summary of one or more campaigns. """ - raise NotImplementedError + def complete(self, campaign_id): + """ Complete an existing campaign (Stop processing events) """ + + return super(API, self).get( + resource_id=campaign_id, resource_action='complete') + + def summary(self, campaign_id=None): + """ Returns the campaign summary """ + resource_cls = CampaignSummary + single_resource = False + + if not campaign_id: + resource_cls = CampaignSummaries + single_resource = True + + return super(API, self).get( + resource_id=campaign_id, + resource_action='summary', + resource_cls=resource_cls, + single_resource=single_resource) - def results(campaign_id): + def results(self, campaign_id): """ Returns just the results for a given campaign """ - raise NotImplementedError + return super(API, self).get( + resource_id=campaign_id, + resource_action='results', + resource_cls=CampaignResults) diff --git a/gophish/api/groups.py b/gophish/api/groups.py index 780d9b7..fb3fea9 100644 --- a/gophish/api/groups.py +++ b/gophish/api/groups.py @@ -1,8 +1,9 @@ from gophish.models import Group from gophish.api import APIEndpoint + class API(APIEndpoint): - def __init__(self, api, endpoint='/api/groups/'): + def __init__(self, api, endpoint='api/groups/'): super(API, self).__init__(api, endpoint=endpoint, cls=Group) def get(self, group_id=None): diff --git a/gophish/api/imap.py b/gophish/api/imap.py new file mode 100644 index 0000000..a7a8e08 --- /dev/null +++ b/gophish/api/imap.py @@ -0,0 +1,34 @@ +from gophish.models import IMAP, Success +from gophish.api import APIEndpoint + + +class API(APIEndpoint): + def __init__(self, api, endpoint='api/imap/'): + super(API, self).__init__(api, endpoint=endpoint, cls=IMAP) + + def get(self): + """Gets the configured IMAP settings + """ + + return super(API, self).get() + + def post(self, imap): + """Updates the IMAP settings + + Arguments: + imap {gophish.models.IMAP} -- The IMAP settings to configure + """ + + return super(API, self).post(imap) + + def validate(self, imap): + """Sends a validation payload to the webhook specified by the given ID + + Arguments: + webhook_id {int} -- The ID of the webhook to validate + """ + return self.request("POST", + body=imap.as_dict(), + resource_action='validate', + resource_cls=Success, + single_resource=True) diff --git a/gophish/api/pages.py b/gophish/api/pages.py index 83574cb..00e3bad 100644 --- a/gophish/api/pages.py +++ b/gophish/api/pages.py @@ -1,14 +1,15 @@ from gophish.models import Page from gophish.api import APIEndpoint + class API(APIEndpoint): - def __init__(self, api, endpoint='/api/pages/'): + def __init__(self, api, endpoint='api/pages/'): super(API, self).__init__(api, endpoint=endpoint, cls=Page) def get(self, page_id=None): """ Gets one or more pages """ - - return self.super(API, self).get(resource_id=page_id) + + return super(API, self).get(resource_id=page_id) def post(self, page): """ Creates a new page """ @@ -18,7 +19,7 @@ def post(self, page): def put(self, page): """ Edits a page """ - return super(API, self).put(put) + return super(API, self).put(page) def delete(self, page_id): """ Deletes a page by ID """ diff --git a/gophish/api/smtp.py b/gophish/api/smtp.py index 11f96a3..2abf509 100644 --- a/gophish/api/smtp.py +++ b/gophish/api/smtp.py @@ -1,8 +1,9 @@ from gophish.models import SMTP from gophish.api import APIEndpoint + class API(APIEndpoint): - def __init__(self, api, endpoint='/api/smtp/'): + def __init__(self, api, endpoint='api/smtp/'): super(API, self).__init__(api, endpoint=endpoint, cls=SMTP) def get(self, smtp_id=None): diff --git a/gophish/api/templates.py b/gophish/api/templates.py index e98cff6..a2a4863 100644 --- a/gophish/api/templates.py +++ b/gophish/api/templates.py @@ -1,13 +1,14 @@ from gophish.models import Template from gophish.api import APIEndpoint + class API(APIEndpoint): - def __init__(self, api, endpoint='/api/templates/'): + def __init__(self, api, endpoint='api/templates/'): super(API, self).__init__(api, endpoint=endpoint, cls=Template) def get(self, template_id=None): """ Gets one or more templates """ - + return super(API, self).get(resource_id=template_id) def post(self, template): diff --git a/gophish/api/webhooks.py b/gophish/api/webhooks.py new file mode 100644 index 0000000..84c067d --- /dev/null +++ b/gophish/api/webhooks.py @@ -0,0 +1,54 @@ +from gophish.models import Webhook +from gophish.api import APIEndpoint + + +class API(APIEndpoint): + def __init__(self, api, endpoint='api/webhooks/'): + super(API, self).__init__(api, endpoint=endpoint, cls=Webhook) + + def get(self, webhook_id=None): + """Gets one or more webhooks + + Keyword Arguments: + webhook_id {int} -- The ID of the Webhook (optional, default: {None}) + """ + + return super(API, self).get(resource_id=webhook_id) + + def post(self, webhook): + """Creates a new webhook + + Arguments: + webhook {gophish.models.Webhook} -- The webhook to create + """ + + return super(API, self).post(webhook) + + def put(self, webhook): + """Edits a webhook + + Arguments: + webhook {gophish.models.Webhook} -- The updated webhook details + """ + + return super(API, self).put(webhook) + + def delete(self, webhook_id): + """Deletes a webhook by ID + + Arguments: + webhook_id {int} -- The ID of the webhook to delete + """ + return super(API, self).delete(webhook_id) + + def validate(self, webhook_id): + """Sends a validation payload to the webhook specified by the given ID + + Arguments: + webhook_id {int} -- The ID of the webhook to validate + """ + return self.request("POST", + resource_id=webhook_id, + resource_action='validate', + resource_cls=Webhook, + single_resource=True) diff --git a/gophish/client.py b/gophish/client.py index d5d92d6..64d9c9f 100644 --- a/gophish/client.py +++ b/gophish/client.py @@ -1,34 +1,45 @@ import requests -from gophish.api import ( - campaigns, - groups, - pages, - smtp, - templates) +from gophish.api import (campaigns, groups, imap, pages, smtp, templates, + webhooks) + +DEFAULT_URL = 'https://localhost:3333' -DEFAULT_URL = 'http://localhost:3333' class GophishClient(object): """ A standard HTTP REST client used by Gophish """ def __init__(self, api_key, host=DEFAULT_URL, **kwargs): self.api_key = api_key - self.host = host + if host.endswith('/'): + self.host = host + else: + self.host = host + '/' self._client_kwargs = kwargs - + def execute(self, method, path, **kwargs): """ Executes a request to a given endpoint, returning the result """ url = "{}{}".format(self.host, path) kwargs.update(self._client_kwargs) - response = requests.request(method, url, params={"api_key": self.api_key}, **kwargs) + response = requests.request( + method, + url, + headers={"Authorization": "Bearer {}".format(self.api_key)}, + **kwargs) return response + class Gophish(object): - def __init__(self, api_key, host=DEFAULT_URL, client=GophishClient, **kwargs): + def __init__(self, + api_key, + host=DEFAULT_URL, + client=GophishClient, + **kwargs): self.client = client(api_key, host=host, **kwargs) self.campaigns = campaigns.API(self.client) self.groups = groups.API(self.client) + self.imap = imap.API(self.client) self.pages = pages.API(self.client) self.smtp = smtp.API(self.client) self.templates = templates.API(self.client) + self.webhooks = webhooks.API(self.client) diff --git a/gophish/models.py b/gophish/models.py index 4bc1784..f1a5044 100644 --- a/gophish/models.py +++ b/gophish/models.py @@ -4,14 +4,20 @@ import json as _json import dateutil.parser + def parse_date(datestr): """ Parses an ISO 8601 formatted date from Gophish """ return dateutil.parser.parse(datestr) + class Model(object): def __init__(self): self._valid_properties = {} + @classmethod + def _is_builtin(cls, obj): + return isinstance(obj, (int, float, str, list, dict, bool)) + def as_dict(self): """ Returns a dict representation of the resource """ result = {} @@ -20,15 +26,23 @@ def as_dict(self): if isinstance(val, datetime): val = val.isoformat() # Parse custom classes - elif val and not isinstance(val, (str, list, dict)): + elif val and not Model._is_builtin(val): val = val.as_dict() # Parse lists of objects elif isinstance(val, list): - val = [e.as_dict() for e in val] + # We only want to call as_dict in the case where the item + # isn't a builtin type. + for i in range(len(val)): + if Model._is_builtin(val[i]): + continue + val[i] = val[i].as_dict() + # If it's a boolean, add it regardless of the value + elif isinstance(val, bool): + result[key] = val # Add it if it's not None if val: - result[key] = val + result[key] = val return result @classmethod @@ -39,16 +53,27 @@ def parse(cls, json): class Campaign(Model): _valid_properties = { - 'id': None, 'name': None, 'created_date': datetime.now(tzlocal()), - 'launch_date': datetime.now(tzlocal()), 'completed_date': None, 'template': None, - 'page': None, 'results': [], 'status': None, 'timeline': [], - 'smtp': None, 'url': None, 'groups': [], 'profile': None} + 'id': None, + 'name': None, + 'created_date': datetime.now(tzlocal()), + 'launch_date': datetime.now(tzlocal()), + 'send_by_date': None, + 'completed_date': None, + 'template': None, + 'page': None, + 'results': [], + 'status': None, + 'timeline': [], + 'smtp': None, + 'url': None, + 'groups': [], + } def __init__(self, **kwargs): """ Creates a new campaign instance """ for key, default in Campaign._valid_properties.items(): setattr(self, key, kwargs.get(key, default)) - + @classmethod def parse(cls, json): campaign = cls() @@ -72,11 +97,126 @@ def parse(cls, json): return campaign +class CampaignSummaries(Model): + ''' Represents a list of campaign summary objects ''' + _valid_properties = {'total': None, 'campaigns': None} + + def __init__(self): + """ Creates a new instance of the campaign summaries""" + for key, default in CampaignSummaries._valid_properties.items(): + setattr(self, key, default) + + @classmethod + def parse(cls, json): + campaign_summaries = cls() + for key, val in json.items(): + # TODO Add date parsing + if key == 'campaigns': + summaries = [CampaignSummary.parse(summary) for summary in val] + setattr(campaign_summaries, key, summaries) + elif key in cls._valid_properties: + setattr(campaign_summaries, key, val) + return campaign_summaries + + +class CampaignSummary(Model): + ''' Represents a campaign summary object ''' + _valid_properties = { + 'id': None, + 'name': None, + 'status': None, + 'created_date': None, + 'send_by_date': None, + 'launch_date': None, + 'completed_date': None, + 'stats': None + } + + def __init__(self): + for key, default in CampaignSummary._valid_properties.items(): + setattr(self, key, default) + + @classmethod + def parse(cls, json): + summary = cls() + for key, val in json.items(): + # TODO Add date parsing + if key == 'stats': + stats = Stat.parse(val) + setattr(summary, key, stats) + elif key in cls._valid_properties: + setattr(summary, key, val) + return summary + + +class Stat(Model): + _valid_properties = { + 'total': None, + 'sent': None, + 'opened': None, + 'clicked': None, + 'submitted_data': None, + 'email_reported': None, + 'error': None + } + + def __init__(self): + for key, default in Stat._valid_properties.items(): + setattr(self, key, default) + + @classmethod + def parse(cls, json): + stat = cls() + for key, val in json.items(): + if key in cls._valid_properties: + setattr(stat, key, val) + return stat + + +class CampaignResults(Model): + ''' Represents a succinct view of campaign results ''' + _valid_properties = { + 'id': None, + 'name': None, + 'results': [], + 'status': None, + 'timeline': [], + } + + def __init__(self, **kwargs): + """ Creates a new instance of the campaign results object""" + for key, default in CampaignResults._valid_properties.items(): + setattr(self, key, kwargs.get(key, default)) + + @classmethod + def parse(cls, json): + campaign_results = cls() + for key, val in json.items(): + # TODO Add date parsing + if key == 'results': + results = [Result.parse(result) for result in val] + setattr(campaign_results, key, results) + elif key == 'timeline': + if val is not None: + timeline = [TimelineEntry.parse(entry) for entry in val] + setattr(campaign_results, key, timeline) + elif key in cls._valid_properties: + setattr(campaign_results, key, val) + return campaign_results + + class Result(Model): _valid_properties = { - 'id': None, 'first_name': None, 'last_name': None, 'email': None, - 'position': None, 'ip': None, 'latitude': None, 'longitude': None, - 'status': None} + 'id': None, + 'first_name': None, + 'last_name': None, + 'email': None, + 'position': None, + 'ip': None, + 'latitude': None, + 'longitude': None, + 'status': None + } def __init__(self, **kwargs): for key, default in Result._valid_properties.items(): @@ -91,8 +231,18 @@ def parse(cls, json): return result -class TimelineEntry(object): - _valid_properties = {'email': None, 'time': None, 'message': None, 'details': None} +class TimelineEntry(Model): + _valid_properties = { + 'email': None, + 'time': None, + 'message': None, + 'details': None + } + + def __init__(self): + ''' Creates a new instance of a timeline entry ''' + for key, default in TimelineEntry._valid_properties.items(): + setattr(self, key, default) @classmethod def parse(cls, json): @@ -110,8 +260,12 @@ class User(Model): """ User contains the attributes for a member of a group used in Gophish """ _valid_properties = { - 'id': None, 'first_name': None, 'last_name': None, 'email': None, - 'position': None} + 'id': None, + 'first_name': None, + 'last_name': None, + 'email': None, + 'position': None + } def __init__(self, **kwargs): for key, default in User._valid_properties.items(): @@ -129,8 +283,11 @@ def parse(cls, json): class Group(Model): """ Groups contain one or more users """ _valid_properties = { - 'id': None, 'name': None, 'modified_date': datetime.now(tzlocal()), - 'targets': []} + 'id': None, + 'name': None, + 'modified_date': datetime.now(tzlocal()), + 'targets': [] + } def __init__(self, **kwargs): for key, default in Group._valid_properties.items(): @@ -152,9 +309,17 @@ def parse(cls, json): class SMTP(Model): _valid_properties = { - 'id': None, 'interface_type': 'SMTP', 'name': None, 'host': None, - 'from_address': None, 'ignore_cert_errors' : False, - 'modified_date': datetime.now(tzlocal())} + 'id': None, + 'interface_type': 'SMTP', + 'name': None, + 'host': None, + 'username': None, + 'password': None, + 'from_address': None, + 'ignore_cert_errors': False, + 'modified_date': datetime.now(tzlocal()), + 'headers': [] + } def __init__(self, **kwargs): for key, default in SMTP._valid_properties.items(): @@ -173,8 +338,14 @@ def parse(cls, json): class Template(Model): _valid_properties = { - 'id': None, 'name': None, 'text': None, 'html': None, - 'modified_date': datetime.now(tzlocal()), 'subject': None, 'attachments': []} + 'id': None, + 'name': None, + 'text': None, + 'html': None, + 'modified_date': datetime.now(tzlocal()), + 'subject': None, + 'attachments': [] + } def __init__(self, **kwargs): for key, default in Template._valid_properties.items(): @@ -186,9 +357,10 @@ def parse(cls, json): for key, val in json.items(): if key == 'modified_date': setattr(template, key, parse_date(val)) - elif key == 'attachments': + elif key == 'attachments' and val: attachments = [ - Attachment.parse(attachment) for attachment in val] + Attachment.parse(attachment) for attachment in val + ] setattr(template, key, attachments) elif key in cls._valid_properties: setattr(template, key, val) @@ -197,9 +369,14 @@ def parse(cls, json): class Page(Model): _valid_properties = { - 'id': None, 'name': None, 'html': None, 'modified_date': datetime.now(tzlocal()), - 'capture_credentials': False, 'capture_passwords': False, - 'redirect_url': None} + 'id': None, + 'name': None, + 'html': None, + 'modified_date': datetime.now(tzlocal()), + 'capture_credentials': False, + 'capture_passwords': False, + 'redirect_url': None + } def __init__(self, **kwargs): for key, default in Page._valid_properties.items(): @@ -215,6 +392,7 @@ def parse(cls, json): setattr(page, key, val) return page + class Attachment(Model): _valid_properties = {'content': None, 'type': None, 'name': None} @@ -226,8 +404,87 @@ def parse(cls, json): setattr(attachment, key, val) return attachment -class Error(Model): - _valid_properties = {'message', 'success', 'data'} + +class Webhook(Model): + _valid_properties = { + 'id': None, + 'name': None, + 'url': None, + 'secret': None, + 'is_active': None + } + + def __init__(self, **kwargs): + for key, default in Webhook._valid_properties.items(): + setattr(self, key, kwargs.get(key, default)) + + @classmethod + def parse(cls, json): + webhook = cls() + for key, val in json.items(): + if key in cls._valid_properties: + setattr(webhook, key, val) + return webhook + + +class IMAP(Model): + _valid_properties = { + 'enabled': None, + 'host': None, + 'port': None, + 'username': None, + 'password': None, + 'tls': None, + 'folder': None, + 'restrict_domain': None, + 'delete_reported_campaign_email': None, + 'last_login': None, + 'modified_date': None, + 'imap_freq': None + } + + def __init__(self, **kwargs): + for key, default in IMAP._valid_properties.items(): + setattr(self, key, kwargs.get(key, default)) + + @classmethod + def parse(cls, json): + imap = cls() + for key, val in json.items(): + if key in cls._valid_properties: + setattr(imap, key, val) + return imap + + +class Success(Exception, Model): + _valid_properties = {'message': None, 'success': None, 'data': None} + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + def __str__(self): + return self.message + + @classmethod + def parse(cls, json): + success = cls() + for key, val in json.items(): + if key in cls._valid_properties: + setattr(success, key, val) + return success + + +class Error(Exception, Model): + _valid_properties = {'message': None, 'success': None, 'data': None} + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + def __str__(self): + return self.message + + def __repr__(self): + return _json.dumps(self.as_dict()) @classmethod def parse(cls, json): diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index f3261e4..0000000 --- a/requirements.txt +++ /dev/null @@ -1,6 +0,0 @@ -appdirs==1.4.0 -packaging==16.8 -pyparsing==2.1.10 -python-dateutil==2.6.0 -requests==2.12.5 -six==1.10.0 diff --git a/setup.py b/setup.py index 4fa6e03..9fc8a07 100644 --- a/setup.py +++ b/setup.py @@ -1,23 +1,36 @@ +"""This is the setup module for the Python Gophish API client.""" from setuptools import setup setup( - name = 'gophish', - packages = ['gophish', 'gophish.api'], - version = '0.1.1', - description = 'Python API Client for Gophish', - author = 'Jordan Wright', - author_email = 'python@getgophish.com', - url = 'https://github.com/gophish/api-client-python', - license='MIT', - download_url = 'https://github.com/gophish/api-client-python/tarball/0.1.1', - keywords = ['gophish'], - classifiers = [ - 'Development Status :: 3 - Alpha', - 'Intended Audience :: Developers', - 'Natural Language :: English', - 'Programming Language :: Python', - 'Programming Language :: Python :: 2.7', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.3', - ], + name="gophish", + packages=["gophish", "gophish.api"], + version="0.5.1", + description="Python API Client for Gophish", + author="Jordan Wright", + author_email="python@getgophish.com", + url="https://github.com/gophish/api-client-python", + license="MIT", + download_url="https://github.com/gophish/api-client-python/tarball/0.5.1", + keywords=["gophish"], + classifiers=[ + "Development Status :: 3 - Alpha", + "Intended Audience :: Developers", + "Natural Language :: English", + "Programming Language :: Python", + "Programming Language :: Python :: 2.7", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.3", + ], + install_requires=[ + "appdirs==1.4.4", + "certifi==2020.6.20", + "chardet==3.0.4", + "idna==2.10", + "packaging==20.4", + "pyparsing==2.4.7", + "python-dateutil==2.8.1", + "requests==2.24.0", + "six==1.15.0", + "urllib3==1.25.10" + ], )