Skip to content

Commit c2c0294

Browse files
committed
Merge pull request #11 from dyninc/compat-update
Compat Update and Much, Much More!
2 parents 1295e89 + e91e287 commit c2c0294

25 files changed

+804
-263
lines changed

.gitignore

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,4 +32,7 @@ nosetests.xml
3232
# Mr Developer
3333
.mr.developer.cfg
3434
.project
35-
.pydevproject
35+
.pydevproject
36+
37+
# PyCharm
38+
.idea/

HISTORY.rst

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,17 @@
11
Release History
22
---------------
3+
1.1.0 (2014-09-16)
4+
++++++++++++++++++
5+
6+
* Internally improved Python2/3 compaability with the intoduction of the dyn.compat module
7+
* Timestamps for various report types are accepted as Python datetime.datetime instances
8+
* Added qps report access to Zones
9+
* Added __str__, __repr__, __unicode__, and __bytes__ methods to all API object types
10+
* Added conditional password encryption to allow for better in-app security
11+
* Added the ability for users to specify their own password encryption keys
12+
* Added __getstate__ and __setstate__ methods to SessionEngine, allowing sessions to be serialized
13+
* Misc bug fixes
14+
315
1.0.3 (2014-09-05)
416
++++++++++++++++++
517

@@ -9,7 +21,7 @@ Release History
921
++++++++++++++++++
1022

1123
* Added reports module
12-
* Updated installation documentation.
24+
* Updated installation documentation
1325

1426
1.0.1 (2014-08-06)
1527
++++++++++++++++++

README.rst

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,30 @@ services.
77
Requires Python 2.6 or higher, or the "simplejson" package.
88

99
For full documentation and examples see the dyn module on `Read The Docs <http://dyn.readthedocs.org>`_.
10+
11+
Installation
12+
------------
13+
14+
To install the dyn SDK, simply:
15+
16+
.. code-block:: bash
17+
18+
$ pip install dyn
19+
20+
21+
Documentation
22+
-------------
23+
24+
Documentation is available on `Read The Docs`_
25+
26+
Contribute
27+
----------
28+
29+
#. Check for open issues or open a new issue to start a discussion around a feature idea or a bug.
30+
#. For bug reports especially it's encouraged for you to include a code snippet highlighting the bug.
31+
#. For feature requests it's encouraged for you to include sample code highlighting a use case for the new feature.
32+
#. Fork `the repository`_ on GitHub to start making your changes to the **master** branch (or branch off of it).
33+
#. Send a pull request and bug the maintainer until it gets merged and published. :)
34+
35+
.. _`the repository`: http://github.com/dyninc/dyn-python
36+
.. _`Read The Docs`: http://dyn.readthedocs.org>

docs/sessions.rst renamed to docs/advanced.rst

Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
.. _sessions:
22

3-
Advanced Sessions Overview
4-
==========================
3+
Advanced Topics
4+
===============
5+
This Section serves as a collective for advanced topics that most developers
6+
using this library will never need to know about, but that may be useful for
7+
developers who are destined to maintain this package
8+
9+
Sessions
10+
--------
511

612
The way in which sessions are handled in this library are designed to be super
713
easy to use for developers who use this library, however, have become relatively
@@ -10,15 +16,15 @@ mainly for developers who would like to contribute to this code base, or who are
1016
just curious as to what is actually going on under the hood.
1117

1218
Parent Class
13-
------------
19+
^^^^^^^^^^^^
1420
Both :class:`dyn.tm.session.DynectSession` and :class:`dyn.mm.session.MMSession`
1521
are subclasses of :class:`dyn.core.SessionEngine`. The :class:`dyn.core.SessionEngine`
1622
provides an easy to use internal API for preparing, sending, and processing outbound
1723
API calls. This class was added in v1.0.0 and greatly reduced the amount of logic
1824
and duplicated code that made looking at these sessions so overly complex.
1925

2026
Parent Type
21-
-----------
27+
^^^^^^^^^^^
2228
Since v0.4.0 sessions had always been implemented as a Singleton type. At this point
2329
you're probably asing "Why?" And that's a bit of a complicated question. One of the main
2430
reasons that these sessions were implemented as a Singleton was to make it easier for
@@ -120,3 +126,39 @@ other instances, since those instances are tied to the classes themselves instea
120126
of held in the *globals* of the session modules. In addition this allows users
121127
to have multiple active sessions across multiple threads, which was previously
122128
impossible in the prior implementation.
129+
130+
131+
Password Encryption
132+
-------------------
133+
The DynECT REST API only accepts passwords in plain text, and currently there is
134+
no way around that. However, for those of you that are particularly mindful of
135+
security (and even those of you who aren't) can probably see some serious pitfalls
136+
to this. As far as most users of this library are concerned the passwords stored in
137+
their :class:`~dyn.tm.session.DynectSession` objects will only ever live in memory,
138+
so it's really not a huge deal that their passwords are stored in plain text. However,
139+
for users looking to do more advanced things, such as serialize and store their session
140+
objects in something less secure, such as a database, then these plain text passwords
141+
are far less than ideal. Because of this in version 1.1.0 we've added optional
142+
AES-256 password encryption for all :class:`~dyn.tm.session.DynectSession`
143+
instances. All you need to do to enable password encryption is install
144+
`PyCrypto <http://www.dlitz.net/software/pycrypto/>`_. The rest will happen
145+
automatically.
146+
147+
Key Generation
148+
^^^^^^^^^^^^^^
149+
Also in version 1.1.0 an optional key field parameter was added to the
150+
:class:`~dyn.tm.session.DynectSession` __init__ method. This field will allow
151+
you to specify the key that your password will be encrypted using. However,
152+
you may also choose to let the dyn module handle the key generation for you as
153+
well using the :func:`~dyn.encrypt.generate_key` function which generates a,
154+
nearly, random 50 character key that can be easily consumed by the
155+
:class:`~dyn.encrypt.AESCipher` class (the class responsible for performing
156+
the actual encryption and decryption.
157+
158+
Encrypt Module
159+
^^^^^^^^^^^^^^
160+
.. autofunction:: dyn.encrypt.generate_key
161+
162+
.. autoclass:: dyn.encrypt.AESCipher
163+
:members:
164+
:undoc-members:

docs/index.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ Contents:
3737
quickstart
3838
tm
3939
mm
40-
sessions
40+
advanced
4141

4242

4343
Indices and tables

docs/tm/reports.rst

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,3 @@ List Functions
1414
.. autofunction:: dyn.tm.reports.get_rttm_rrset
1515
.. autofunction:: dyn.tm.reports.get_qps
1616
.. autofunction:: dyn.tm.reports.get_zone_notes
17-

docs/tm/services/gslb/gslb.rst

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,3 +66,23 @@ the dyn.tm System and how to edit some of the same fields mentioned above.
6666
>>> fqdn = zone + '.'
6767
>>> gslb = GSLB(zone, fqdn)
6868

69+
Replacing a GSLB Monitor
70+
^^^^^^^^^^^^^^^^^^^^^^^^
71+
If you'd like to create a brand new :class:`Monitor` for your GSLB service, rather
72+
than update your existing one, the following example shows how simple it is to
73+
accomplish this task
74+
::
75+
76+
>>> from dyn.tm.services.gslb import GSLB, Monitor
77+
>>> zone = 'example.com'
78+
>>> fqdn = zone + '.'
79+
>>> gslb = GSLB(zone, fqdn)
80+
>>> gslb.monitor.protocol
81+
'HTTP'
82+
>>> expected_text = "This is the text you're looking for."
83+
>>> new_monitor = Monitor('HTTPS', 10, timeout=500, port=5005,
84+
expected=expected_text)
85+
>>> gslb.monitor = new_monitor
86+
>>> gslb.monitor.protocol
87+
'HTTPS'
88+

dyn/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
66
Requires Python 2.6 or higher, or the "simplejson" package.
77
"""
8-
version_info = (1, 0, 3)
8+
version_info = (1, 1, 0)
99
__name__ = 'dyn'
1010
__doc__ = 'A python wrapper for the DynDNS and DynEmail APIs'
1111
__author__ = 'Jonathan Nappi, Cole Tuininga'

dyn/compat.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
# -*- coding: utf-8 -*-
2+
"""python 2-3 compatability layer. The bulk of this was borrowed from
3+
kennethreitz's requests module
4+
"""
5+
import sys
6+
7+
# -------
8+
# Pythons
9+
# -------
10+
11+
# Syntax sugar.
12+
_ver = sys.version_info
13+
14+
#: Python 2.x?
15+
is_py2 = (_ver[0] == 2)
16+
17+
#: Python 3.x?
18+
is_py3 = (_ver[0] == 3)
19+
20+
# -----------------
21+
# Version Specifics
22+
# -----------------
23+
24+
if is_py2:
25+
# If we have no JSON-esque module installed, we can't do anything
26+
try:
27+
import json
28+
except ImportError as ex:
29+
try:
30+
import simplejson as json
31+
except ImportError:
32+
raise ex
33+
from httplib import HTTPConnection, HTTPSConnection, HTTPException
34+
from urllib import urlencode, pathname2url
35+
36+
string_types = (str, unicode)
37+
38+
def prepare_to_send(args):
39+
return bytes(args)
40+
41+
def prepare_for_loads(body, encoding):
42+
return body
43+
44+
def force_unicode(s, encoding='UTF-8'):
45+
try:
46+
s = unicode(s)
47+
except UnicodeDecodeError:
48+
s = str(s).decode(encoding, 'replace')
49+
50+
return s
51+
52+
elif is_py3:
53+
from http.client import HTTPConnection, HTTPSConnection, HTTPException
54+
from urllib.parse import urlencode
55+
from urllib.request import pathname2url
56+
import json
57+
string_types = (str,)
58+
59+
def prepare_to_send(args):
60+
return bytes(args, 'UTF-8')
61+
62+
def prepare_for_loads(body, encoding):
63+
return body.decode(encoding)
64+
65+
def force_unicode(s, encoding='UTF-8'):
66+
return str(s)

dyn/core.py

Lines changed: 39 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -4,25 +4,15 @@
44
library, it is not recommened and could possible result in some strange
55
behavior.
66
"""
7-
import sys
87
import time
98
import locale
109
import logging
1110
import threading
1211
from datetime import datetime
13-
try:
14-
import json
15-
except ImportError:
16-
try:
17-
import simplejson as json
18-
except ImportError:
19-
sys.exit('Could not find json or simplejson libraries.')
20-
if sys.version_info[0] == 2:
21-
from httplib import HTTPConnection, HTTPSConnection, HTTPException
22-
elif sys.version_info[0] == 3:
23-
from http.client import HTTPConnection, HTTPSConnection, HTTPException
24-
# API Libs
25-
from dyn import __version__
12+
13+
from . import __version__
14+
from .compat import (HTTPConnection, HTTPSConnection, HTTPException, json,
15+
is_py2, is_py3, prepare_to_send, force_unicode)
2616

2717

2818
def cleared_class_dict(dict_obj):
@@ -107,7 +97,7 @@ def close_session(cls):
10797
key = getattr(cls, '__metakey__')
10898
closed = cls._instances.get(key, {}).pop(cur_thread, None)
10999
if len(cls._instances.get(key, {})) == 0:
110-
del cls._instances[key]
100+
cls._instances.pop(key, None)
111101
return closed
112102

113103
@property
@@ -164,11 +154,7 @@ def _handle_response(self, response, uri, method, raw_args, final):
164154
if self.poll_incomplete:
165155
response, body = self.poll_response(response, body)
166156
self._last_response = response
167-
ret_val = None
168-
if sys.version_info[0] == 2:
169-
ret_val = json.loads(body)
170-
elif sys.version_info[0] == 3:
171-
ret_val = json.loads(body.decode('UTF-8'))
157+
ret_val = json.loads(body.decode('UTF-8'))
172158

173159
self._meta_update(uri, method, ret_val)
174160
# Handle retrying if ZoneProp is blocking the current task
@@ -225,15 +211,24 @@ def execute(self, uri, method, args=None, final=False):
225211
:param final: boolean flag representing whether or not we have already
226212
failed executing once or not
227213
"""
214+
if self._conn is None:
215+
self.connect()
216+
228217
uri = self._validate_uri(uri)
229218

230219
# Make sure the method is valid
231220
self._validate_method(method)
232221

233-
self.logger.debug('uri: {}, method: {}, args: {}'.format(uri, method,
234-
args))
235222
# Prepare arguments to send to API
236223
raw_args, args, uri = self._prepare_arguments(args, method, uri)
224+
225+
# Don't display password when debug logging
226+
cleaned_args = json.loads(args)
227+
if 'password' in cleaned_args:
228+
cleaned_args['password'] = '*****'
229+
230+
self.logger.debug('uri: {}, method: {}, args: {}'.format(uri, method,
231+
cleaned_args))
237232
# Send the command and deal with results
238233
self.send_command(uri, method, args)
239234

@@ -314,11 +309,7 @@ def send_command(self, uri, method, args):
314309
self._conn.putheader('Content-length', '%d' % len(args))
315310
self._conn.endheaders()
316311

317-
if sys.version_info[0] == 2:
318-
self._conn.send(bytes(args))
319-
elif sys.version_info[0] == 3:
320-
# noinspection PyArgumentList
321-
self._conn.send(bytes(args, 'UTF-8'))
312+
self._conn.send(prepare_to_send(args))
322313

323314
def wait_for_job_to_complete(self, job_id, timeout=120):
324315
"""When a response comes back with a status of "incomplete" we need to
@@ -343,7 +334,27 @@ def wait_for_job_to_complete(self, job_id, timeout=120):
343334
response = self.execute(uri, 'GET', api_args)
344335
return response
345336

337+
def __getstate__(cls):
338+
"""Because HTTP/HTTPS connections are not serializeable, we need to
339+
strip the connection instance out before we ship the pickled data
340+
"""
341+
d = cls.__dict__.copy()
342+
d.pop('_conn')
343+
return d
344+
345+
def __setstate__(cls, state):
346+
"""Because the HTTP/HTTPS connection was stripped out in __getstate__ we
347+
must manually re-enter it as None and let the sessions execute method
348+
handle rebuilding it later
349+
"""
350+
cls.__dict__ = state
351+
cls.__dict__['_conn'] = None
352+
346353
def __str__(self):
347354
"""str override"""
348-
return '<{}>'.format(self.name)
355+
return force_unicode('<{}>').format(self.name)
349356
__repr__ = __unicode__ = __str__
357+
358+
def __bytes__(self):
359+
"""bytes override"""
360+
return bytes(self.__str__())

0 commit comments

Comments
 (0)