Skip to content

Commit

Permalink
add documentation and metadata for release
Browse files Browse the repository at this point in the history
  • Loading branch information
davidism committed Jan 28, 2015
1 parent 884d74f commit e55b391
Show file tree
Hide file tree
Showing 8 changed files with 255 additions and 16 deletions.
124 changes: 119 additions & 5 deletions .hgignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,120 @@
syntax: glob
.idea
*.egg*
build
dist
*.pyc

# https://github.com/github/gitignore/blob/master/Python.gitignore

# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]

# C extensions
*.so

# Distribution / packaging
.Python
env/
build/
develop-eggs/
dist/
eggs/
lib/
lib64/
parts/
sdist/
var/
*.egg-info/
.installed.cfg
*.egg

# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec

# Installer logs
pip-log.txt
pip-delete-this-directory.txt

# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.cache
nosetests.xml
coverage.xml

# Translations
*.mo
*.pot

# Django stuff:
*.log

# Sphinx documentation
docs/_build/

# PyBuilder
target/

# https://github.com/github/gitignore/blob/master/Global/JetBrains.gitignore
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm

## Directory-based project format
.idea/
# if you remove the above rule, at least ignore user-specific stuff:
# .idea/workspace.xml
# .idea/tasks.xml
# and these sensitive or high-churn files:
# .idea/dataSources.ids
# .idea/dataSources.xml
# .idea/sqlDataSources.xml
# .idea/dynamic.xml

## File-based project format
*.ipr
*.iml
*.iws

## Additional for IntelliJ
out/

# generated by mpeltonen/sbt-idea plugin
.idea_modules/

# generated by JIRA plugin
atlassian-ide-plugin.xml

# generated by Crashlytics plugin (for Android Studio and Intellij)
com_crashlytics_export_strings.xml

# https://github.com/github/gitignore/blob/master/Global/Linux.gitignore

*~

# KDE directory preferences
.directory

# https://github.com/github/gitignore/blob/master/Global/OSX.gitignore

.DS_Store
.AppleDouble
.LSOverride

# Icon must end with two \r
Icon


# Thumbnails
._*

# Files that might appear on external disk
.Spotlight-V100
.Trashes

# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items

# Project specific
1 change: 1 addition & 0 deletions AUTHORS.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
* `David Lord <https://bitbucket.org/davidism>`_
11 changes: 11 additions & 0 deletions LICENSE.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
Copyright (c) 2014 by Moebius Solutions, Inc. and contributors. See AUTHORS.rst for more details.

Some rights reserved.

Redistribution and use in source and binary forms of the software as well as documentation, with or without modification, are permitted provided that the following conditions are met:

* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
* The names of the contributors may not be used to endorse or promote products derived from this software without specific prior written permission.

THIS SOFTWARE AND DOCUMENTATION IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE AND DOCUMENTATION, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
1 change: 1 addition & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
include AUTHORS.rst LICENSE.rst
41 changes: 41 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
Python client for McAfee ePolicy Orchestrator
=============================================

A straightforward wrapper around the ePO API.
Manages authentication, building requests, and interpreting responses.
Simply treat the client object as a callable function, passing the command name and parameters.

Install::

$ pip install mcafee-epo

Use::

>>> from mcafee_epo import Client
>>> client = Client('https://localhost:8443', 'user', 'password')
>>> systems = client('system.find', '')

Differences from "official" client
----------------------------------

This library was created in response to the fairly poor client distributed by McAfee, which didn't support Python 3 and
was generally a mess.
(You can find a cleaned up version of their client with Python 3 support in the first few commits.)

The official library required copying files into the Python location to "install" it.
This library is an actual package installed using ``pip``.

The official client uses low level url libraries and numerous workarounds to make http requests.
This library uses the `requests <http://python-requests.org/>`_ library to greatly simplify the work the previous code
was doing.

The official client used a dynamic command discovery and dispatch mechanism to make api calls seem like a nested set of
objects.
This library forgoes that complexity (which wasn't understood by IDEs anyway) for a more straightforward approach that
just accepts command names when calling.

Links
-----

* `PyPI releases <https://pypi.python.org/pypi/mcafee-epo>`_
* `Source code <https://bitbucket.org/davidism/mcafee-epo>`_
66 changes: 58 additions & 8 deletions mcafee_epo.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,27 @@
except ImportError:
from urllib.parse import urljoin

__version__ = '1.0'

class APIException(Exception):
pass

class APIError(Exception):
"""Represents an error with the data received within a valid HTTP response."""


class Client:
"""Communicate with an ePO server.
Instances are callable, pass a command name and parameters to make API calls.
"""

def __init__(self, url, username, password, session=None):
"""Create a client for the given ePO server.
:param url: location of ePO server
:param username: username to authenticate
:param password: password to authenticate
:param session: custom instance of :class:`requests.Session`, optional
"""

self.url = url
self.username = username
self.password = password
Expand All @@ -24,32 +38,68 @@ def __init__(self, url, username, password, session=None):
self._token = None

def _get_token(self, _skip=False):
"""Get the security token if it's not already cached.
:param bool _skip: used internally when making the initial request to get the token
"""

if self._token is None and not _skip:
self._token = self._request('core.getSecurityToken', _skip_token=True)
self._token = self._request('core.getSecurityToken')

return self._token

def _request(self, name, _skip_token=False, **kwargs):
def _request(self, name, **kwargs):
"""Format the request and interpret the response.
Usually you want to use :meth:`__call__` instead.
:param name: command name to call
:param kwargs: arguments passed to :meth:`requests.request`
:return: deserialized JSON data
"""

kwargs.setdefault('auth', (self.username, self.password))
params = kwargs.setdefault('params', {})
params.setdefault(':output', 'json')
params.setdefault('orion.user.security.token', self._get_token(_skip=_skip_token))
# check whether the response will be json (default)
is_json = params.setdefault(':output', 'json') == 'json'
# add the security token, unless this is the request to get the token
params.setdefault('orion.user.security.token', self._get_token(_skip=name == 'core.getSecurityToken'))
url = urljoin(self.url, 'remote/{}'.format(name))

if kwargs.get('data') or kwargs.get('json') or kwargs.get('files'):
# use post method if there is post data
r = self._session.post(url, **kwargs)
else:
r = self._session.get(url, **kwargs)

# check that there was a valid http response
r.raise_for_status()
text = r.text

if not text.startswith('OK:'):
raise APIException(text)
# response body contains an error
raise APIError(text)

return json.loads(text[3:])
return json.loads(text[3:]) if is_json else text[3:]

def __call__(self, name, *args, params=None, files=None, **kwargs):
"""Make an API call by calling this instance.
Collects arguments and calls :meth:`_request`.
ePO commands take positional and named arguments. Positional arguments are internally numbered "param#" and
passed as named arguments.
Files can be passed to some commands. Pass a dictionary of ``'filename': file-like objects``, or other formats
accepted by :meth:`requests.request`. This command will not open files, as it is better to manage that in a
``with`` block in calling code.
:param str name: command name to call
:param args: positional arguments to command
:param kwargs: named arguments to command
:param dict params: named arguments that are not valid Python names can be provided here
:param dict files: files to upload to command
:return: deserialized JSON data
"""

if params is None:
params = {}

Expand Down
5 changes: 5 additions & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[egg_info]
tag_build = .dev0

[aliases]
release = egg_info -RDb ''
22 changes: 19 additions & 3 deletions setup.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,31 @@
#!/usr/bin/env python
import os
import re
from setuptools import setup

with open(os.path.join(os.path.dirname(__file__), 'mcafee_epo.py')) as f:
version = re.search(r"__version__ = '(.*)'", f.read()).group(1)

setup(
name='McAfee-ePO',
version='1.0',
url='https://community.mcafee.com/docs/DOC-3095',
name='mcafee-epo',
version=version,
url='https://bitbucket.org/davidism/mcafee-epo',
license='BSD',
author='David Lord',
author_email='[email protected]',
description='McAfee ePolicy Orchestrator API',
classifiers=[
'Development Status :: 5 - Production/Stable',
'Intended Audience :: Developers',
'License :: OSI Approved :: BSD License',
'Operating System :: OS Independent',
'Programming Language :: Python',
'Programming Language :: Python :: 2',
'Programming Language :: Python :: 3',
'Topic :: System :: Systems Administration'
],
py_modules=['mcafee_epo'],
include_package_data=True,
zip_safe=True,
install_requires=['requests']
)

0 comments on commit e55b391

Please sign in to comment.