[python-osmapi] 01/03: Imported Upstream version 0.5.0

Bas Couwenberg sebastic at xs4all.nl
Sat Jan 3 18:20:02 UTC 2015


This is an automated email from the git hooks/post-receive script.

sebastic-guest pushed a commit to branch master
in repository python-osmapi.

commit 3b20a0beed65568404a1a69955371410d7b0cf0d
Author: Bas Couwenberg <sebastic at xs4all.nl>
Date:   Sat Jan 3 17:35:07 2015 +0100

    Imported Upstream version 0.5.0
---
 .coveragerc                                        |    5 +
 .gitignore                                         |    5 +
 .travis.yml                                        |   42 +
 CHANGELOG.md                                       |  167 ++
 LICENSE.txt                                        |  674 +++++++
 MANIFEST.in                                        |    1 +
 README.md                                          |   95 +
 build.sh                                           |   20 +
 divshot.json                                       |    7 +
 osmapi/OsmApi.py                                   | 2030 ++++++++++++++++++++
 osmapi/__init__.py                                 |    5 +
 requirements.txt                                   |    5 +
 setup.cfg                                          |    5 +
 setup.py                                           |   44 +
 test-requirements.txt                              |    8 +
 tests/__init__.py                                  |    0
 tests/capabilities_test.py                         |   23 +
 tests/changeset_tests.py                           |  700 +++++++
 tests/fixtures/test_Capabilities.xml               |   12 +
 tests/fixtures/test_ChangesetClose.xml             |    0
 tests/fixtures/test_ChangesetComment.xml           |    7 +
 tests/fixtures/test_ChangesetCreate.xml            |    1 +
 .../test_ChangesetCreate_with_created_by.xml       |    1 +
 .../test_ChangesetCreate_with_open_changeset.xml   |    1 +
 tests/fixtures/test_ChangesetDownload.xml          |   87 +
 tests/fixtures/test_ChangesetGet.xml               |    7 +
 tests/fixtures/test_ChangesetGetWithComment.xml    |   18 +
 tests/fixtures/test_ChangesetSubscribe.xml         |    7 +
 ...est_ChangesetSubscribeWhenAlreadySubscribed.xml |    1 +
 tests/fixtures/test_ChangesetUnsubscribe.xml       |    7 +
 .../test_ChangesetUnsubscribeWhenNotSubscribed.xml |    1 +
 tests/fixtures/test_ChangesetUpdate.xml            |    1 +
 .../test_ChangesetUpdate_with_created_by.xml       |    1 +
 .../fixtures/test_ChangesetUpdate_wo_changeset.xml |    0
 .../fixtures/test_ChangesetUpload_create_node.xml  |    5 +
 .../test_ChangesetUpload_delete_relation.xml       |    4 +
 tests/fixtures/test_ChangesetUpload_modify_way.xml |    4 +
 tests/fixtures/test_ChangesetsGet.xml              |   39 +
 tests/fixtures/test_NodeCreate.xml                 |    1 +
 tests/fixtures/test_NodeCreate_changesetauto.xml   |    1 +
 tests/fixtures/test_NodeDelete.xml                 |    1 +
 tests/fixtures/test_NodeGet.xml                    |    8 +
 tests/fixtures/test_NodeGet_with_version.xml       |    6 +
 tests/fixtures/test_NodeHistory.xml                |   29 +
 tests/fixtures/test_NodeRelations.xml              |   13 +
 tests/fixtures/test_NodeUpdate.xml                 |    1 +
 tests/fixtures/test_NodeWays.xml                   |   13 +
 tests/fixtures/test_NodesGet.xml                   |    9 +
 tests/fixtures/test_NoteClose.xml                  |   31 +
 tests/fixtures/test_NoteComment.xml                |   31 +
 tests/fixtures/test_NoteCommentAnonymous.xml       |   25 +
 tests/fixtures/test_NoteCreate.xml                 |   22 +
 tests/fixtures/test_NoteCreateAnonymous.xml        |   19 +
 tests/fixtures/test_NoteCreate_wo_auth.xml         |   32 +
 tests/fixtures/test_NoteGet.xml                    |   31 +
 tests/fixtures/test_NoteReopen.xml                 |   40 +
 tests/fixtures/test_NotesGet.xml                   |  441 +++++
 tests/fixtures/test_NotesSearch.xml                |   61 +
 tests/fixtures/test_RelationCreate.xml             |    1 +
 tests/fixtures/test_RelationDelete.xml             |    1 +
 tests/fixtures/test_RelationFull.xml               |   43 +
 tests/fixtures/test_RelationGet.xml                |   17 +
 tests/fixtures/test_RelationGet_with_version.xml   |   23 +
 tests/fixtures/test_RelationHistory.xml            |   17 +
 tests/fixtures/test_RelationRelations.xml          |  131 ++
 tests/fixtures/test_RelationUpdate.xml             |    1 +
 tests/fixtures/test_RelationsGet.xml               |  157 ++
 tests/fixtures/test_WayCreate.xml                  |    1 +
 tests/fixtures/test_WayDelete.xml                  |    1 +
 tests/fixtures/test_WayFull.xml                    |   41 +
 tests/fixtures/test_WayGet.xml                     |   24 +
 tests/fixtures/test_WayGet_nodata.xml              |    0
 tests/fixtures/test_WayGet_with_version.xml        |   22 +
 tests/fixtures/test_WayHistory.xml                 |   40 +
 tests/fixtures/test_WayRelations.xml               |   13 +
 tests/fixtures/test_WayUpdate.xml                  |    1 +
 tests/fixtures/test_WaysGet.xml                    |   31 +
 tests/helper_tests.py                              |  117 ++
 tests/node_tests.py                                |  317 +++
 tests/notes_tests.py                               |  375 ++++
 tests/osmapi_tests.py                              |   76 +
 tests/relation_tests.py                            |  283 +++
 tests/way_tests.py                                 |  243 +++
 tox.ini                                            |   11 +
 84 files changed, 6842 insertions(+)

diff --git a/.coveragerc b/.coveragerc
new file mode 100644
index 0000000..7511801
--- /dev/null
+++ b/.coveragerc
@@ -0,0 +1,5 @@
+[report]
+omit =
+    */python?.?/*
+    */site-packages/nose/*
+    tests/*
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..706a49c
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,5 @@
+dist/
+MANIFEST
+*.pyc
+.coverage
+.tox
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..f4beaa1
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,42 @@
+language: python
+
+python:
+- '2.6'
+- '2.7'
+- '3.2'
+- '3.3'
+
+before_install:
+- sudo apt-get update -qq
+- sudo apt-get install -qq pandoc
+
+install:
+- if [[ $TRAVIS_PYTHON_VERSION == 2.6 ]]; then
+     pip install --use-mirrors unittest2;
+  fi
+- pip install .
+- pip install -r requirements.txt
+- pip install -r test-requirements.txt
+
+script: ./build.sh
+
+after_success: coveralls
+
+deploy:
+  - provider: pypi
+    user: odi
+    password:
+      secure: MU3ZQ4rcpsXo0xIYSWXBfaKTAPn1IrL7AEcH231sseFV1RVmdC96Sfmtc2llvD9Eoc0KJpdW0Vy50azNqAMJwXCt/q3gagfao1PTnAEbklU+g1s2PTqW401E95Qm6w192WzWk/q0dy3SJwxEQt023QR78K+nEcYaCdLWDHjR2hY=
+    on:
+      branch: master
+      python: 2.7
+      repo: metaodi/osmapi
+  - provider: divshot
+    skip_cleanup: true
+    environment:
+        develop: development
+        master: production
+    on:
+      python: 2.7
+    api_key:
+      secure: WTpFZfmA9QOPGLp9c+yljSVLk8GijvlMT4FxPX1BlI0cdU0VHHBkKfRk+UW3n8vHBAcaH+1SlbPc+9BWw9oHirtJ5Wc57SNfWS1NHKVB/rUxVCn+GYM6RBN9wJ26St0KNG81VtvajkA41ZP5jkpWVFtbylbctcTmVNagJke7cUE=
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..02d3060
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,167 @@
+# Change Log
+All notable changes to this project will be documented in this file.
+This project follows [Semantic Versioning](http://semver.org/).
+
+## [Unreleased][unreleased]
+
+## 0.5.0 - 2015-01-03
+### Changed
+- BC-break: all dates are now parsed as datetime objects
+
+### Added
+- Implementation for changeset discussions (ChangesetComment, ChangesetSubscribe, ChangesetUnsubscribe)
+- When (un)subscribing to a changeset, there are two special errors `AlreadySubscribedApiError` and `NotSubscribedApiError` to check for
+- The ChangesetGet method got a new parameter `include_discussion` to determine wheter or not changeset discussion should be in the response
+
+## 0.4.2 - 2015-01-01
+### Fixed
+- Result of `NodeWay` is now actually parsed as a `way`
+
+### Added
+- Lots of method comments for documentation
+
+### Changed
+- Update to pdoc 0.3.1 which changed the appearance of the online docs
+
+## 0.4.1 - 2014-10-08
+### Changed
+- Parse dates in notes as `datetime` objects
+
+## 0.4.0 - 2014-10-07
+### Added
+- Release for OSM Notes API
+- Generation of online documentation (http://osmapi.divshot.io)
+
+## 0.3.1 - 2014-06-21
+### Fixed
+- Hotfix release of Python 3.x (base64)
+
+## 0.3.0 - 2014-05-20
+### Added
+- Support for Python 3.x
+- Use `tox` to run tests against multiple versions of Python
+
+## 0.2.26 - 2014-05-02
+### Fixed
+- Fixed notes again
+
+## 0.2.25 - 2014-05-02
+### Fixed
+- Unit tests for basic functionality
+- Fixed based on the unit tests (previously undetected bugs)
+
+## 0.2.24 - 2014-01-07
+### Fixed
+- Fixed notes
+
+## 0.2.23 - 2014-01-03
+### Changed
+- Hotfix release
+
+## 0.2.22 - 2014-01-03
+### Fixed
+- Fixed README.md not found error during installation
+
+## 0.2.21 - 2014-01-03
+### Changed
+- Updated description
+
+## 0.2.20 - 2014-01-01
+### Added
+- First release of PyPI package "osmapi"
+
+## 0.2.19 - 2014-01-01
+### Changed
+- Inital version from SVN (http://svn.openstreetmap.org/applications/utils/python_lib/OsmApi/OsmApi.py)
+- Move to GitHub
+
+## 0.2.19 - 2010-05-24
+### Changed
+- Add debug message on ApiError
+
+## 0.2.18 - 2010-04-20
+### Fixed
+- Fix ChangesetClose and _http_request
+
+## 0.2.17 - 2010-01-02
+### Added
+- Capabilities implementation
+
+## 0.2.16 - 2010-01-02
+### Changed
+- ChangesetsGet by Alexander Rampp
+
+## 0.2.15 - 2009-12-16
+### Fixed
+- xml encoding error for < and >
+
+## 0.2.14 - 2009-11-20
+### Changed
+- changesetautomulti parameter
+
+## 0.2.13 - 2009-11-16
+### Changed
+- modify instead update for osc
+
+## 0.2.12 - 2009-11-14
+### Added
+- raise ApiError on 4xx errors
+
+## 0.2.11 - 2009-10-14
+### Fixed
+- unicode error on ChangesetUpload
+
+## 0.2.10 - 2009-10-14
+### Added
+- RelationFullRecur definition
+
+## 0.2.9  - 2009-10-13
+### Added
+- automatic changeset management
+- ChangesetUpload implementation
+
+## 0.2.8  - 2009-10-13
+### Changed
+- *(Create|Update|Delete) use not unique _do method
+
+## 0.2.7  - 2009-10-09
+### Added
+- implement all missing functions except ChangesetsGet and GetCapabilities
+
+## 0.2.6  - 2009-10-09
+### Changed
+- encoding clean-up
+
+## 0.2.5  - 2009-10-09
+### Added
+- implements NodesGet, WaysGet, RelationsGet, ParseOsm, ParseOsc
+
+## 0.2.4  - 2009-10-06 clean-up
+### Changed
+- clean-up
+
+## 0.2.3  - 2009-09-09 
+### Changed
+- keep http connection alive for multiple request
+- (Node|Way|Relation)Get return None when object have been deleted (raising error before)
+
+## 0.2.2  - 2009-07-13
+### Added
+- can identify applications built on top of the lib
+
+## 0.2.1  - 2009-05-05
+### Changed
+- some changes in constructor
+
+## 0.2    - 2009-05-01
+### Added
+- initial import
+
+
+# Categories
+- `Added` for new features.
+- `Changed` for changes in existing functionality.
+- `Deprecated` for once-stable features removed in upcoming releases.
+- `Removed` for deprecated features removed in this release.
+- `Fixed` for any bug fixes.
+- `Security` to invite users to upgrade in case of vulnerabilities.
diff --git a/LICENSE.txt b/LICENSE.txt
new file mode 100644
index 0000000..94a9ed0
--- /dev/null
+++ b/LICENSE.txt
@@ -0,0 +1,674 @@
+                    GNU GENERAL PUBLIC LICENSE
+                       Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+                            Preamble
+
+  The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+  The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works.  By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users.  We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors.  You can apply it to
+your programs, too.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+  To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights.  Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+  For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received.  You must make sure that they, too, receive
+or can get the source code.  And you must show them these terms so they
+know their rights.
+
+  Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+  For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software.  For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+  Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so.  This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software.  The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable.  Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products.  If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+  Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary.  To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+
+                       TERMS AND CONDITIONS
+
+  0. Definitions.
+
+  "This License" refers to version 3 of the GNU General Public License.
+
+  "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+  "The Program" refers to any copyrightable work licensed under this
+License.  Each licensee is addressed as "you".  "Licensees" and
+"recipients" may be individuals or organizations.
+
+  To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy.  The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+  A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+  To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy.  Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+  To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies.  Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+  An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License.  If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+  1. Source Code.
+
+  The "source code" for a work means the preferred form of the work
+for making modifications to it.  "Object code" means any non-source
+form of a work.
+
+  A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+  The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form.  A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+  The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities.  However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work.  For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+  The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+  The Corresponding Source for a work in source code form is that
+same work.
+
+  2. Basic Permissions.
+
+  All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met.  This License explicitly affirms your unlimited
+permission to run the unmodified Program.  The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work.  This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+  You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force.  You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright.  Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+  Conveying under any other circumstances is permitted solely under
+the conditions stated below.  Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+  3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+  No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+  When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+  4. Conveying Verbatim Copies.
+
+  You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+  You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+  5. Conveying Modified Source Versions.
+
+  You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+    a) The work must carry prominent notices stating that you modified
+    it, and giving a relevant date.
+
+    b) The work must carry prominent notices stating that it is
+    released under this License and any conditions added under section
+    7.  This requirement modifies the requirement in section 4 to
+    "keep intact all notices".
+
+    c) You must license the entire work, as a whole, under this
+    License to anyone who comes into possession of a copy.  This
+    License will therefore apply, along with any applicable section 7
+    additional terms, to the whole of the work, and all its parts,
+    regardless of how they are packaged.  This License gives no
+    permission to license the work in any other way, but it does not
+    invalidate such permission if you have separately received it.
+
+    d) If the work has interactive user interfaces, each must display
+    Appropriate Legal Notices; however, if the Program has interactive
+    interfaces that do not display Appropriate Legal Notices, your
+    work need not make them do so.
+
+  A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit.  Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+  6. Conveying Non-Source Forms.
+
+  You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+    a) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by the
+    Corresponding Source fixed on a durable physical medium
+    customarily used for software interchange.
+
+    b) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by a
+    written offer, valid for at least three years and valid for as
+    long as you offer spare parts or customer support for that product
+    model, to give anyone who possesses the object code either (1) a
+    copy of the Corresponding Source for all the software in the
+    product that is covered by this License, on a durable physical
+    medium customarily used for software interchange, for a price no
+    more than your reasonable cost of physically performing this
+    conveying of source, or (2) access to copy the
+    Corresponding Source from a network server at no charge.
+
+    c) Convey individual copies of the object code with a copy of the
+    written offer to provide the Corresponding Source.  This
+    alternative is allowed only occasionally and noncommercially, and
+    only if you received the object code with such an offer, in accord
+    with subsection 6b.
+
+    d) Convey the object code by offering access from a designated
+    place (gratis or for a charge), and offer equivalent access to the
+    Corresponding Source in the same way through the same place at no
+    further charge.  You need not require recipients to copy the
+    Corresponding Source along with the object code.  If the place to
+    copy the object code is a network server, the Corresponding Source
+    may be on a different server (operated by you or a third party)
+    that supports equivalent copying facilities, provided you maintain
+    clear directions next to the object code saying where to find the
+    Corresponding Source.  Regardless of what server hosts the
+    Corresponding Source, you remain obligated to ensure that it is
+    available for as long as needed to satisfy these requirements.
+
+    e) Convey the object code using peer-to-peer transmission, provided
+    you inform other peers where the object code and Corresponding
+    Source of the work are being offered to the general public at no
+    charge under subsection 6d.
+
+  A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+  A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling.  In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage.  For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product.  A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+  "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source.  The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+  If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information.  But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+  The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed.  Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+  Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+  7. Additional Terms.
+
+  "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law.  If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+  When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it.  (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.)  You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+  Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+    a) Disclaiming warranty or limiting liability differently from the
+    terms of sections 15 and 16 of this License; or
+
+    b) Requiring preservation of specified reasonable legal notices or
+    author attributions in that material or in the Appropriate Legal
+    Notices displayed by works containing it; or
+
+    c) Prohibiting misrepresentation of the origin of that material, or
+    requiring that modified versions of such material be marked in
+    reasonable ways as different from the original version; or
+
+    d) Limiting the use for publicity purposes of names of licensors or
+    authors of the material; or
+
+    e) Declining to grant rights under trademark law for use of some
+    trade names, trademarks, or service marks; or
+
+    f) Requiring indemnification of licensors and authors of that
+    material by anyone who conveys the material (or modified versions of
+    it) with contractual assumptions of liability to the recipient, for
+    any liability that these contractual assumptions directly impose on
+    those licensors and authors.
+
+  All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10.  If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term.  If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+  If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+  Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+  8. Termination.
+
+  You may not propagate or modify a covered work except as expressly
+provided under this License.  Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+  However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+  Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+  Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License.  If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+  9. Acceptance Not Required for Having Copies.
+
+  You are not required to accept this License in order to receive or
+run a copy of the Program.  Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance.  However,
+nothing other than this License grants you permission to propagate or
+modify any covered work.  These actions infringe copyright if you do
+not accept this License.  Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+  10. Automatic Licensing of Downstream Recipients.
+
+  Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License.  You are not responsible
+for enforcing compliance by third parties with this License.
+
+  An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations.  If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+  You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License.  For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+  11. Patents.
+
+  A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based.  The
+work thus licensed is called the contributor's "contributor version".
+
+  A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version.  For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+  Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+  In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement).  To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+  If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients.  "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+  If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+  A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License.  You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+  Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+  12. No Surrender of Others' Freedom.
+
+  If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all.  For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+  13. Use with the GNU Affero General Public License.
+
+  Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work.  The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+  14. Revised Versions of this License.
+
+  The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time.  Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+  Each version is given a distinguishing version number.  If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation.  If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+  If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+  Later license versions may give you additional or different
+permissions.  However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+  15. Disclaimer of Warranty.
+
+  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+  16. Limitation of Liability.
+
+  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+  17. Interpretation of Sections 15 and 16.
+
+  If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+                     END OF TERMS AND CONDITIONS
+
+            How to Apply These Terms to Your New Programs
+
+  If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+  To do so, attach the following notices to the program.  It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the program's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+Also add information on how to contact you by electronic and paper mail.
+
+  If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+    <program>  Copyright (C) <year>  <name of author>
+    This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+    This is free software, and you are welcome to redistribute it
+    under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License.  Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+  You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+<http://www.gnu.org/licenses/>.
+
+  The GNU General Public License does not permit incorporating your program
+into proprietary programs.  If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library.  If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.  But first, please read
+<http://www.gnu.org/philosophy/why-not-lgpl.html>.
diff --git a/MANIFEST.in b/MANIFEST.in
new file mode 100644
index 0000000..bb3ec5f
--- /dev/null
+++ b/MANIFEST.in
@@ -0,0 +1 @@
+include README.md
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..ff6aa6a
--- /dev/null
+++ b/README.md
@@ -0,0 +1,95 @@
+osmapi
+======
+
+[![Build](https://travis-ci.org/metaodi/osmapi.png?branch=develop)](https://travis-ci.org/metaodi/osmapi)
+[![Coverage](https://coveralls.io/repos/metaodi/osmapi/badge.png?branch=develop)](https://coveralls.io/r/metaodi/osmapi?branch=develop)
+[![Version](https://badge.fury.io/py/osmapi.png)](http://badge.fury.io/py/osmapi)
+[![Downloads](https://pypip.in/d/osmapi/badge.png)](https://pypi.python.org/pypi/osmapi/)
+[![License](https://pypip.in/license/osmapi/badge.png)](https://pypi.python.org/pypi/osmapi/)
+
+Python wrapper for the OSM API
+
+## Installation
+
+Install `osmapi` simply by using pip:
+
+    pip install osmapi
+
+### Development
+
+If you want to help with the development of `osmapi`, you should clone this repository and install the requirements:
+
+    pip install -r requirements.txt
+    pip install -r test-requirements.txt
+
+After that, it is recommended to install the `flake8` pre-commit-hook:
+
+    flake8 --install-hook
+
+### Tests
+
+To run the tests use the following command:
+
+    nosetests --verbose
+
+By using tox you can even run the tests against different versions of python (2.6, 2.7, 3.2 and 3.3):
+
+    tox
+
+## Note
+
+Scripted imports and automated edits should only be carried out by those with experience and understanding of the way the OpenStreetMap community creates maps, and only with careful **planning** and **consultation** with the local community.
+
+See the [Import/Guidelines](http://wiki.openstreetmap.org/wiki/Import/Guidelines) and [Automated Edits/Code of Conduct](http://wiki.openstreetmap.org/wiki/Automated_Edits/Code_of_Conduct) for more information.
+
+## Documentation
+
+The documentation is generated using `pdoc` and can be [viewed online](http://osmapi.divshot.io/).
+
+The build the documentation locally, you can use
+
+    pdoc --html osmapi.OsmApi # create HTML file
+
+## Examples
+
+### Read from OpenStreetMap
+
+```python
+import osmapi
+api = osmapi.OsmApi()
+print api.NodeGet(123)
+# {u'changeset': 532907, u'uid': 14298,
+# u'timestamp': u'2007-09-29T09:19:17Z',
+# u'lon': 10.790009299999999, u'visible': True,
+# u'version': 1, u'user': u'Mede',
+# u'lat': 59.9503044, u'tag': {}, u'id': 123}
+```
+
+### Constructor
+
+```python
+import osmapi
+api = osmapi.OsmApi(api="api06.dev.openstreetmap.org", username = "you", password = "***")
+api = osmapi.OsmApi(username = "you", passwordfile = "/etc/mypasswords")
+api = osmapi.OsmApi(passwordfile = "/etc/mypasswords") # username will be first line username
+```
+
+Note: The password file should have the format _user:password_
+
+### Write to OpenStreetMap
+
+```python
+import osmapi
+api = osmapi.OsmApi(username = u"metaodi", password = u"*******")
+api.ChangesetCreate({u"comment": u"My first test"})
+print api.NodeCreate({u"lon":1, u"lat":1, u"tag": {}})
+# {u'changeset': 532907, u'lon': 1, u'version': 1, u'lat': 1, u'tag': {}, u'id': 164684}
+api.ChangesetClose()
+```
+
+## Attribution
+
+This project was orginally developed by Etienne Chové.
+This repository is a copy of the original code from SVN (http://svn.openstreetmap.org/applications/utils/python_lib/OsmApi/OsmApi.py), with the goal to enable easy contribution via GitHub and release of this package via [PyPI](https://pypi.python.org/pypi/osmapi).
+
+See also the OSM wiki: http://wiki.openstreetmap.org/wiki/Osmapi
diff --git a/build.sh b/build.sh
new file mode 100755
index 0000000..64a4f81
--- /dev/null
+++ b/build.sh
@@ -0,0 +1,20 @@
+#!/bin/bash
+
+set -e
+
+function cleanup {
+    exit $?
+}
+
+trap "cleanup" EXIT
+
+# Check PEP-8 code style and McCabe complexity
+flake8 --show-pep8 --show-source .
+
+# run tests
+nosetests --verbose --with-coverage
+
+# generate docs (currently it's not possible to generate docs in Python 2.6)
+if [[ $TRAVIS_PYTHON_VERSION != 2.6 ]]; then
+    pdoc --html --overwrite osmapi/OsmApi.py
+fi
diff --git a/divshot.json b/divshot.json
new file mode 100644
index 0000000..06e7861
--- /dev/null
+++ b/divshot.json
@@ -0,0 +1,7 @@
+{
+    "name": "osmapi",
+    "root": ".",
+    "routes": {
+        "**": "OsmApi.m.html"
+    }
+}
diff --git a/osmapi/OsmApi.py b/osmapi/OsmApi.py
new file mode 100644
index 0000000..74affbb
--- /dev/null
+++ b/osmapi/OsmApi.py
@@ -0,0 +1,2030 @@
+# -*- coding: utf-8 -*-
+
+"""
+The OsmApi module is a wrapper for the OpenStreetMap API.
+As such it provides an easy access to the functionality of the API.
+
+You can find this module [on PyPI](https://pypi.python.org/pypi/osmapi)
+or [on GitHub](https://github.com/metaodi/osmapi).
+
+Find all information about changes of the different versions of this module
+[in the CHANGELOG](https://github.com/metaodi/osmapi/blob/master/CHANGELOG.md).
+
+
+## Notes:
+
+* **dictionary keys** are _unicode_
+* **changeset** is _integer_
+* **version** is _integer_
+* **tag** is a _dictionary_
+* **timestamp** is _unicode_
+* **user** is _unicode_
+* **uid** is _integer_
+* node **lat** and **lon** are _floats_
+* way **nd** is list of _integers_
+* relation **member** is a _list of dictionaries_ like
+`{"role": "", "ref":123, "type": "node"}`
+
+"""
+
+from __future__ import (absolute_import, print_function, unicode_literals)
+try:
+    import httplib
+except ImportError:
+    import http.client as httplib
+import base64
+import xml.dom.minidom
+import time
+import sys
+import urllib
+from datetime import datetime
+
+# Python 3.x
+if getattr(urllib, 'urlencode', None) is None:
+    urllib.urlencode = urllib.parse.urlencode
+
+from osmapi import __version__
+
+
+class UsernamePasswordMissingError(Exception):
+    """
+    Error when username or password is missing for an authenticated request
+    """
+    pass
+
+
+class ApiError(Exception):
+    """
+    Error class, is thrown when an API request fails
+    """
+
+    def __init__(self, status, reason, payload):
+        self.status = status
+        """HTTP error code"""
+
+        self.reason = reason
+        """Error message"""
+
+        self.payload = payload
+        """Payload of API when this error occured"""
+
+    def __str__(self):
+        return (
+            "Request failed: %s - %s - %s"
+            % (str(self.status), self.reason, self.payload)
+        )
+
+
+class AlreadySubscribedApiError(ApiError):
+    pass
+
+
+class NotSubscribedApiError(ApiError):
+    pass
+
+
+class OsmApi:
+    """
+    Main class of osmapi, instanciate this class to use osmapi
+    """
+
+    def __init__(
+            self,
+            username=None,
+            password=None,
+            passwordfile=None,
+            appid="",
+            created_by="osmapi/"+__version__,
+            api="www.openstreetmap.org",
+            changesetauto=False,
+            changesetautotags={},
+            changesetautosize=500,
+            changesetautomulti=1,
+            debug=False):
+        """
+        Initialized the OsmApi object.
+
+        There are two different ways to authenticate a user.
+        Either `username` and `password` are supplied directly or the path
+        to a `passwordfile` is given, where on the first line username
+        and password must be colon-separated (<user>:<pass>).
+
+        To credit the application that supplies changes to OSM, an `appid`
+        can be provided.  This is a string identifying the application.
+        If this is omitted "osmapi" is used.
+
+        It is possible to configure the URL to connect to using the `api`
+        parameter.  By default this is the production API of OpenStreetMap,
+        for testing purposes, one might prefer the official test instance at
+        "api06.dev.openstreetmap.org".
+
+        There are several options to control the changeset behaviour. By
+        default, a programmer has to take care to open and close a changeset
+        prior to make changes to OSM.
+        By setting `changesetauto` to `True`, osmapi automatically opens
+        changesets.
+        The `changesetautotags` parameter takes a `dict`, where each key/value
+        pair is applied as tags to the changeset.
+        The option `changesetautosize` defines the size of each
+        upload (default: 500) and `changesetautomulti` defines how many
+        uploads should be made before closing a changeset and opening a new
+        one (default: 1).
+
+        The `debug` parameter can be used to generate a more verbose output.
+        """
+
+        # debug
+        self._debug = debug
+
+        # Get username
+        if username:
+            self._username = username
+        elif passwordfile:
+            pass_line = open(passwordfile).readline()
+            self._username = pass_line.split(":")[0].strip()
+
+        # Get password
+        if password:
+            self._password = password
+        elif passwordfile:
+            for l in open(passwordfile).readlines():
+                l = l.strip().split(":")
+                if l[0] == self._username:
+                    self._password = l[1]
+
+        # Changest informations
+        # auto create and close changesets
+        self._changesetauto = changesetauto
+        # tags for automatic created changesets
+        self._changesetautotags = changesetautotags
+        # change count for auto changeset
+        self._changesetautosize = changesetautosize
+        # change count for auto changeset
+        self._changesetautosize = changesetautosize
+        # close a changeset every # upload
+        self._changesetautomulti = changesetautomulti
+        self._changesetautocpt = 0
+        # data to upload for auto group
+        self._changesetautodata = []
+
+        # Get API
+        self._api = api
+
+        # Get created_by
+        if not appid:
+            self._created_by = created_by
+        else:
+            self._created_by = appid + " (" + created_by + ")"
+
+        # Initialisation
+        self._CurrentChangesetId = 0
+
+        # Http connection
+        self._conn = self._get_http_connection()
+
+    def __del__(self):
+        if self._changesetauto:
+            self._changesetautoflush(True)
+        return None
+
+    ##################################################
+    # Capabilities                                   #
+    ##################################################
+
+    def Capabilities(self):
+        """
+        Returns the API capabilities as a dict:
+
+            #!python
+            {
+                'area': {
+                    'maximum': area in square degrees that can be queried,
+                },
+                'changesets': {
+                    'maximum_elements': number of elements per changeset,
+                },
+                'status': {
+                    'api': online|readonly|offline,
+                    'database': online|readonly|offline,
+                    'gpx': online|readonly|offline,
+                },
+                'timeout': {
+                    'seconds': timeout in seconds for API calls,
+                },
+                'tracepoints': {
+                    'per_page': maximum number of points in a GPX track,
+                },
+                'version': {
+                    'maximum': maximum version of API this server supports,
+                    'minimum': minimum version of API this server supports,
+                },
+                'waynodes': {
+                    'maximum': maximum number of nodes that a way may contain,
+                },
+            }
+
+        The capabilities can be used by a client to
+        gain insights of the server in use.
+        """
+        uri = "/api/capabilities"
+        data = self._get(uri)
+        data = xml.dom.minidom.parseString(data)
+        data = data.getElementsByTagName("osm")[0]
+        data = data.getElementsByTagName("api")[0]
+        result = {}
+        for elem in data.childNodes:
+            if elem.nodeType != elem.ELEMENT_NODE:
+                continue
+            result[elem.nodeName] = {}
+            for k, v in elem.attributes.items():
+                try:
+                    result[elem.nodeName][k] = float(v)
+                except:
+                    result[elem.nodeName][k] = v
+        return result
+
+    ##################################################
+    # Node                                           #
+    ##################################################
+
+    def NodeGet(self, NodeId, NodeVersion=-1):
+        """
+        Returns node with `NodeId` as a dict:
+
+            #!python
+            {
+                'id': id of node,
+                'lat': latitude of node,
+                'lon': longitude of node,
+                'tag': {},
+                'changeset': id of changeset of last change,
+                'version': version number of node,
+                'user': username of user that made the last change,
+                'uid': id of user that made the last change,
+                'timestamp': timestamp of last change,
+                'visible': True|False
+            }
+
+        If `NodeVersion` is supplied, this specific version is returned,
+        otherwise the latest version is returned.
+        """
+        uri = "/api/0.6/node/"+str(NodeId)
+        if NodeVersion != -1:
+            uri += "/"+str(NodeVersion)
+        data = self._get(uri)
+        if not data:
+            return data
+        data = xml.dom.minidom.parseString(data)
+        data = data.getElementsByTagName("osm")[0]
+        data = data.getElementsByTagName("node")[0]
+        return self._DomParseNode(data)
+
+    def NodeCreate(self, NodeData):
+        """
+        Creates a node based on the supplied `NodeData` dict:
+
+            #!python
+            {
+                'lat': latitude of node,
+                'lon': longitude of node,
+                'tag': {},
+            }
+
+        Returns updated `NodeData` (without timestamp):
+
+            #!python
+            {
+                'id': id of node,
+                'lat': latitude of node,
+                'lon': longitude of node,
+                'tag': dict of tags,
+                'changeset': id of changeset of last change,
+                'version': version number of node,
+                'user': username of last change,
+                'uid': id of user of last change,
+                'visible': True|False
+            }
+        """
+        return self._do("create", "node", NodeData)
+
+    def NodeUpdate(self, NodeData):
+        """
+        Updates node with the supplied `NodeData` dict:
+
+            #!python
+            {
+                'id': id of node,
+                'lat': latitude of node,
+                'lon': longitude of node,
+                'tag': {},
+                'version': version number of node,
+            }
+
+        Returns updated `NodeData` (without timestamp):
+
+            #!python
+            {
+                'id': id of node,
+                'lat': latitude of node,
+                'lon': longitude of node,
+                'tag': dict of tags,
+                'changeset': id of changeset of last change,
+                'version': version number of node,
+                'user': username of last change,
+                'uid': id of user of last change,
+                'visible': True|False
+            }
+        """
+        return self._do("modify", "node", NodeData)
+
+    def NodeDelete(self, NodeData):
+        """
+        Delete node with `NodeData`:
+
+            #!python
+            {
+                'id': id of node,
+                'lat': latitude of node,
+                'lon': longitude of node,
+                'tag': dict of tags,
+                'version': version number of node,
+            }
+
+        Returns updated `NodeData` (without timestamp):
+
+            #!python
+            {
+                'id': id of node,
+                'lat': latitude of node,
+                'lon': longitude of node,
+                'tag': dict of tags,
+                'changeset': id of changeset of last change,
+                'version': version number of node,
+                'user': username of last change,
+                'uid': id of user of last change,
+                'visible': True|False
+            }
+        """
+        return self._do("delete", "node", NodeData)
+
+    def NodeHistory(self, NodeId):
+        """
+        Returns dict with version as key:
+
+            #!python
+            {
+                '1': dict of NodeData,
+                '2': dict of NodeData,
+                ...
+            }
+
+        `NodeId` is the unique identifier of a node.
+        """
+        uri = "/api/0.6/node/"+str(NodeId)+"/history"
+        data = self._get(uri)
+        data = xml.dom.minidom.parseString(data)
+        result = {}
+        osm_data = data.getElementsByTagName("osm")[0]
+        for data in osm_data.getElementsByTagName("node"):
+            data = self._DomParseNode(data)
+            result[data["version"]] = data
+        return result
+
+    def NodeWays(self, NodeId):
+        """
+        Returns a list of dicts of `WayData` containing node `NodeId`:
+
+            #!python
+            [
+                {
+                    'id': id of Way,
+                    'nd': [] list of NodeIds in this way
+                    'tag': {} dict of tags,
+                    'changeset': id of changeset of last change,
+                    'version': version number of Way,
+                    'user': username of user that made the last change,
+                    'uid': id of user that made the last change,
+                    'visible': True|False
+                },
+                {
+                    ...
+                },
+            ]
+
+        The `NodeId` is a unique identifier for a node.
+        """
+        uri = "/api/0.6/node/%d/ways" % NodeId
+        data = self._get(uri)
+        data = xml.dom.minidom.parseString(data)
+        result = []
+        osm_data = data.getElementsByTagName("osm")[0]
+        for data in osm_data.getElementsByTagName("way"):
+            data = self._DomParseWay(data)
+            result.append(data)
+        return result
+
+    def NodeRelations(self, NodeId):
+        """
+        Returns a list of dicts of `RelationData` containing node `NodeId`:
+
+            #!python
+            [
+                {
+                    'id': id of Relation,
+                    'member': [
+                        {
+                            'ref': ID of referenced element,
+                            'role': optional description of role in relation
+                            'type': node|way|relation
+                        },
+                        {
+                            ...
+                        }
+                    ]
+                    'tag': {},
+                    'changeset': id of changeset of last change,
+                    'version': version number of Way,
+                    'user': username of user that made the last change,
+                    'uid': id of user that made the last change,
+                    'visible': True|False
+                },
+                {
+                    ...
+                },
+            ]
+
+        The `NodeId` is a unique identifier for a node.
+        """
+        uri = "/api/0.6/node/%d/relations" % NodeId
+        data = self._get(uri)
+        data = xml.dom.minidom.parseString(data)
+        result = []
+        osm_data = data.getElementsByTagName("osm")[0]
+        for data in osm_data.getElementsByTagName("relation"):
+            data = self._DomParseRelation(data)
+            result.append(data)
+        return result
+
+    def NodesGet(self, NodeIdList):
+        """
+        Returns dict with the id of the Node as a key
+        for each node in `NodeIdList`:
+
+            #!python
+            {
+                '1234': dict of NodeData,
+                '5678': dict of NodeData,
+                ...
+            }
+
+        `NodeIdList` is a list containing unique identifiers
+        for multiple nodes.
+        """
+        uri = "/api/0.6/nodes?nodes=" + ",".join([str(x) for x in NodeIdList])
+        data = self._get(uri)
+        data = xml.dom.minidom.parseString(data)
+        result = {}
+        osm_data = data.getElementsByTagName("osm")[0]
+        for data in osm_data.getElementsByTagName("node"):
+            data = self._DomParseNode(data)
+            result[data["id"]] = data
+        return result
+
+    ##################################################
+    # Way                                            #
+    ##################################################
+
+    def WayGet(self, WayId, WayVersion=-1):
+        """
+        Returns way with `WayId` as a dict:
+
+            #!python
+            {
+                'id': id of way,
+                'tag': {} tags of this way,
+                'nd': [] list of nodes belonging to this way
+                'changeset': id of changeset of last change,
+                'version': version number of way,
+                'user': username of user that made the last change,
+                'uid': id of user that made the last change,
+                'timestamp': timestamp of last change,
+                'visible': True|False
+            }
+
+        If `WayVersion` is supplied, this specific version is returned,
+        otherwise the latest version is returned.
+        """
+        uri = "/api/0.6/way/"+str(WayId)
+        if WayVersion != -1:
+            uri += "/"+str(WayVersion)
+        data = self._get(uri)
+        if not data:
+            return data
+        data = xml.dom.minidom.parseString(data)
+        data = data.getElementsByTagName("osm")[0]
+        data = data.getElementsByTagName("way")[0]
+        return self._DomParseWay(data)
+
+    def WayCreate(self, WayData):
+        """
+        Creates a way based on the supplied `WayData` dict:
+
+            #!python
+            {
+                'nd': [] list of nodes,
+                'tag': {} dict of tags,
+            }
+
+        Returns updated `WayData` (without timestamp):
+
+            #!python
+            {
+                'id': id of node,
+                'nd': [] list of nodes,
+                'tag': {} dict of tags,
+                'changeset': id of changeset of last change,
+                'version': version number of way,
+                'user': username of last change,
+                'uid': id of user of last change,
+                'visible': True|False
+            }
+        """
+        return self._do("create", "way", WayData)
+
+    def WayUpdate(self, WayData):
+        """
+        Updates way with the supplied `WayData` dict:
+
+            #!python
+            {
+                'id': id of way,
+                'nd': [] list of nodes,
+                'tag': {},
+                'version': version number of way,
+            }
+
+        Returns updated `WayData` (without timestamp):
+
+            #!python
+            {
+                'id': id of node,
+                'nd': [] list of nodes,
+                'tag': {} dict of tags,
+                'changeset': id of changeset of last change,
+                'version': version number of way,
+                'user': username of last change,
+                'uid': id of user of last change,
+                'visible': True|False
+            }
+        """
+        return self._do("modify", "way", WayData)
+
+    def WayDelete(self, WayData):
+        """
+        Delete way with `WayData`:
+
+            #!python
+            {
+                'id': id of way,
+                'nd': [] list of nodes,
+                'tag': dict of tags,
+                'version': version number of way,
+            }
+
+        Returns updated `WayData` (without timestamp):
+
+            #!python
+            {
+                'id': id of node,
+                'nd': [] list of nodes,
+                'tag': {} dict of tags,
+                'changeset': id of changeset of last change,
+                'version': version number of way,
+                'user': username of last change,
+                'uid': id of user of last change,
+                'visible': True|False
+            }
+        """
+        return self._do("delete", "way", WayData)
+
+    def WayHistory(self, WayId):
+        """
+        Returns dict with version as key:
+
+            #!python
+            {
+                '1': dict of WayData,
+                '2': dict of WayData,
+                ...
+            }
+
+        `WayId` is the unique identifier of a way.
+        """
+        uri = "/api/0.6/way/"+str(WayId)+"/history"
+        data = self._get(uri)
+        data = xml.dom.minidom.parseString(data)
+        result = {}
+        osm_data = data.getElementsByTagName("osm")[0]
+        for data in osm_data.getElementsByTagName("way"):
+            data = self._DomParseWay(data)
+            result[data["version"]] = data
+        return result
+
+    def WayRelations(self, WayId):
+        """
+        Returns a list of dicts of `RelationData` containing way `WayId`:
+
+            #!python
+            [
+                {
+                    'id': id of Relation,
+                    'member': [
+                        {
+                            'ref': ID of referenced element,
+                            'role': optional description of role in relation
+                            'type': node|way|relation
+                        },
+                        {
+                            ...
+                        }
+                    ]
+                    'tag': {} dict of tags,
+                    'changeset': id of changeset of last change,
+                    'version': version number of Way,
+                    'user': username of user that made the last change,
+                    'uid': id of user that made the last change,
+                    'visible': True|False
+                },
+                {
+                    ...
+                },
+            ]
+
+        The `WayId` is a unique identifier for a way.
+        """
+        uri = "/api/0.6/way/%d/relations" % WayId
+        data = self._get(uri)
+        data = xml.dom.minidom.parseString(data)
+        result = []
+        osm_data = data.getElementsByTagName("osm")[0]
+        for data in osm_data.getElementsByTagName("relation"):
+            data = self._DomParseRelation(data)
+            result.append(data)
+        return result
+
+    def WayFull(self, WayId):
+        """
+        Returns the full data for way `WayId` as list of dicts:
+
+            #!python
+            [
+                {
+                    'type': node|way|relation,
+                    'data': {} data dict for node|way|relation
+                },
+                { ... }
+            ]
+
+        The `WayId` is a unique identifier for a way.
+        """
+        uri = "/api/0.6/way/"+str(WayId)+"/full"
+        data = self._get(uri)
+        return self.ParseOsm(data)
+
+    def WaysGet(self, WayIdList):
+        """
+        Returns dict with the id of the way as a key for
+        each way in `WayIdList`:
+
+            #!python
+            {
+                '1234': dict of WayData,
+                '5678': dict of WayData,
+                ...
+            }
+
+        `WayIdList` is a list containing unique identifiers for multiple ways.
+        """
+        uri = "/api/0.6/ways?ways=" + ",".join([str(x) for x in WayIdList])
+        data = self._get(uri)
+        data = xml.dom.minidom.parseString(data)
+        result = {}
+        osm_data = data.getElementsByTagName("osm")[0]
+        for data in osm_data.getElementsByTagName("way"):
+            data = self._DomParseWay(data)
+            result[data["id"]] = data
+        return result
+
+    ##################################################
+    # Relation                                       #
+    ##################################################
+
+    def RelationGet(self, RelationId, RelationVersion=-1):
+        """
+        Returns relation with `RelationId` as a dict:
+
+            #!python
+            {
+                'id': id of Relation,
+                'member': [
+                    {
+                        'ref': ID of referenced element,
+                        'role': optional description of role in relation
+                        'type': node|way|relation
+                    },
+                    {
+                        ...
+                    }
+                ]
+                'tag': {} dict of tags,
+                'changeset': id of changeset of last change,
+                'version': version number of Relation,
+                'user': username of user that made the last change,
+                'uid': id of user that made the last change,
+                'timestamp': timestamp of last change,
+                'visible': True|False
+            }
+
+        If `RelationVersion` is supplied, this specific version is returned,
+        otherwise the latest version is returned.
+        """
+        uri = "/api/0.6/relation/"+str(RelationId)
+        if RelationVersion != -1:
+            uri += "/"+str(RelationVersion)
+        data = self._get(uri)
+        if not data:
+            return data
+        data = xml.dom.minidom.parseString(data)
+        data = data.getElementsByTagName("osm")[0]
+        data = data.getElementsByTagName("relation")[0]
+        return self._DomParseRelation(data)
+
+    def RelationCreate(self, RelationData):
+        """
+        Creates a relation based on the supplied `RelationData` dict:
+
+            #!python
+            {
+                'member': [] list of members,
+                'tag': {} dict of tags,
+            }
+
+        Returns updated `RelationData` (without timestamp):
+
+            #!python
+            {
+                'id': id of Relation,
+                'member': [
+                    {
+                        'ref': ID of referenced element,
+                        'role': optional description of role in relation
+                        'type': node|way|relation
+                    },
+                    {
+                        ...
+                    }
+                ]
+                'tag': {} dict of tags,
+                'changeset': id of changeset of last change,
+                'version': version number of Relation,
+                'user': username of user that made the last change,
+                'uid': id of user that made the last change,
+                'visible': True|False
+            }
+        """
+        return self._do("create", "relation", RelationData)
+
+    def RelationUpdate(self, RelationData):
+        """
+        Updates relation with the supplied `RelationData` dict:
+
+            #!python
+            {
+                'id': id of relation,
+                'member': [] list of member dicts,
+                'tag': {},
+                'version': version number of relation,
+            }
+
+        Returns updated `RelationData` (without timestamp):
+
+            #!python
+            {
+                'id': id of Relation,
+                'member': [
+                    {
+                        'ref': ID of referenced element,
+                        'role': optional description of role in relation
+                        'type': node|way|relation
+                    },
+                    {
+                        ...
+                    }
+                ]
+                'tag': {} dict of tags
+                'changeset': id of changeset of last change,
+                'version': version number of Relation,
+                'user': username of user that made the last change,
+                'uid': id of user that made the last change,
+                'visible': True|False
+            }
+        """
+        return self._do("modify", "relation", RelationData)
+
+    def RelationDelete(self, RelationData):
+        """
+        Delete relation with `RelationData` dict:
+
+            #!python
+            {
+                'id': id of relation,
+                'member': [] list of member dicts,
+                'tag': {},
+                'version': version number of relation,
+            }
+
+        Returns updated `RelationData` (without timestamp):
+
+            #!python
+            {
+                'id': id of Relation,
+                'member': [
+                    {
+                        'ref': ID of referenced element,
+                        'role': optional description of role in relation
+                        'type': node|way|relation
+                    },
+                    {
+                        ...
+                    }
+                ]
+                'tag': {} dict of tags,
+                'changeset': id of changeset of last change,
+                'version': version number of Relation,
+                'user': username of user that made the last change,
+                'uid': id of user that made the last change,
+                'visible': True|False
+            }
+        """
+        return self._do("delete", "relation", RelationData)
+
+    def RelationHistory(self, RelationId):
+        """
+        Returns dict with version as key:
+
+            #!python
+            {
+                '1': dict of RelationData,
+                '2': dict of RelationData,
+                ...
+            }
+
+        `RelationId` is the unique identifier of a relation.
+        """
+        uri = "/api/0.6/relation/"+str(RelationId)+"/history"
+        data = self._get(uri)
+        data = xml.dom.minidom.parseString(data)
+        result = {}
+        osm_data = data.getElementsByTagName("osm")[0]
+        for data in osm_data.getElementsByTagName("relation"):
+            data = self._DomParseRelation(data)
+            result[data["version"]] = data
+        return result
+
+    def RelationRelations(self, RelationId):
+        """
+        Returns a list of dicts of `RelationData`
+        containing relation `RelationId`:
+
+            #!python
+            [
+                {
+                    'id': id of Relation,
+                    'member': [
+                        {
+                            'ref': ID of referenced element,
+                            'role': optional description of role in relation
+                            'type': node|way|relation
+                        },
+                        {
+                            ...
+                        }
+                    ]
+                    'tag': {} dict of tags,
+                    'changeset': id of changeset of last change,
+                    'version': version number of Way,
+                    'user': username of user that made the last change,
+                    'uid': id of user that made the last change,
+                    'visible': True|False
+                },
+                {
+                    ...
+                },
+            ]
+
+        The `RelationId` is a unique identifier for a relation.
+        """
+        uri = "/api/0.6/relation/%d/relations" % RelationId
+        data = self._get(uri)
+        data = xml.dom.minidom.parseString(data)
+        result = []
+        osm_data = data.getElementsByTagName("osm")[0]
+        for data in osm_data.getElementsByTagName("relation"):
+            data = self._DomParseRelation(data)
+            result.append(data)
+        return result
+
+    def RelationFullRecur(self, RelationId):
+        """
+        Returns the full data (all levels) for relation
+        `RelationId` as list of dicts:
+
+            #!python
+            [
+                {
+                    'type': node|way|relation,
+                    'data': {} data dict for node|way|relation
+                },
+                { ... }
+            ]
+
+        The `RelationId` is a unique identifier for a way.
+
+        This function is useful for relations containing other relations.
+
+        If you don't need all levels, use `osmapi.OsmApi.RelationFull`
+        instead, which return only 2 levels.
+        """
+        data = []
+        todo = [RelationId]
+        done = []
+        while todo:
+            rid = todo.pop(0)
+            done.append(rid)
+            temp = self.RelationFull(rid)
+            for item in temp:
+                if item["type"] != "relation":
+                    continue
+                if item["data"]["id"] in done:
+                    continue
+                todo.append(item["data"]["id"])
+            data += temp
+        return data
+
+    def RelationFull(self, RelationId):
+        """
+        Returns the full data (two levels) for relation
+        `RelationId` as list of dicts:
+
+            #!python
+            [
+                {
+                    'type': node|way|relation,
+                    'data': {} data dict for node|way|relation
+                },
+                { ... }
+            ]
+
+        The `RelationId` is a unique identifier for a way.
+
+        If you need all levels, use `osmapi.OsmApi.RelationFullRecur`.
+        """
+        uri = "/api/0.6/relation/"+str(RelationId)+"/full"
+        data = self._get(uri)
+        return self.ParseOsm(data)
+
+    def RelationsGet(self, RelationIdList):
+        """
+        Returns dict with the id of the relation as a key
+        for each relation in `RelationIdList`:
+
+            #!python
+            {
+                '1234': dict of RelationData,
+                '5678': dict of RelationData,
+                ...
+            }
+
+        `RelationIdList` is a list containing unique identifiers
+        for multiple relations.
+        """
+        relation_list = ",".join([str(x) for x in RelationIdList])
+        uri = "/api/0.6/relations?relations=" + relation_list
+        data = self._get(uri)
+        data = xml.dom.minidom.parseString(data)
+        result = {}
+        osm_data = data.getElementsByTagName("osm")[0]
+        for data in osm_data.getElementsByTagName("relation"):
+            data = self._DomParseRelation(data)
+            result[data["id"]] = data
+        return result
+
+    ##################################################
+    # Changeset                                      #
+    ##################################################
+
+    def ChangesetGet(self, ChangesetId, include_discussion=False):
+        """
+        Returns changeset with `ChangesetId` as a dict:
+
+            #!python
+            {
+                'id': id of Changeset,
+                'open': True|False, wheter or not this changeset is open
+                'tag': {} dict of tags,
+                'created_at': timestamp of creation of this changeset
+                'closed_at': timestamp when changeset was closed
+                'comments_count': amount of comments
+                'discussion': [] list of comment dict (-> `include_discussion`)
+                'max_lon': maximum longitude of changes in this changeset
+                'max_lat': maximum latitude of changes in this changeset
+                'min_lon': minimum longitude of changes in this changeset
+                'min_lat': minimum longitude of changes in this changeset
+                'user': username of user that created this changeset,
+                'uid': id of user that created this changeset,
+            }
+
+        `ChangesetId` is the unique identifier of a changeset.
+
+        If `include_discussion` is set to `True` the changeset discussion
+        will be available in the result.
+        """
+        path = "/api/0.6/changeset/"+str(ChangesetId)
+        if (include_discussion):
+            path = path + "?include_discussion=true"
+        data = self._get(path)
+        data = xml.dom.minidom.parseString(data)
+        data = data.getElementsByTagName("osm")[0]
+        data = data.getElementsByTagName("changeset")[0]
+        return self._DomParseChangeset(data)
+
+    def ChangesetUpdate(self, ChangesetTags={}):
+        """
+        Updates current changeset with `ChangesetTags`.
+        """
+        if not self._CurrentChangesetId:
+            raise Exception("No changeset currently opened")
+        if "created_by" not in ChangesetTags:
+            ChangesetTags["created_by"] = self._created_by
+        self._put(
+            "/api/0.6/changeset/" + str(self._CurrentChangesetId),
+            self._XmlBuild("changeset", {"tag": ChangesetTags})
+        )
+        return self._CurrentChangesetId
+
+    def ChangesetCreate(self, ChangesetTags={}):
+        """
+        Opens a changeset.
+
+        If `ChangesetTags` are given, this tags are applied (key/value).
+
+        Returns `ChangesetId`
+        """
+        if self._CurrentChangesetId:
+            raise Exception("Changeset already opened")
+        if "created_by" not in ChangesetTags:
+            ChangesetTags["created_by"] = self._created_by
+        result = self._put(
+            "/api/0.6/changeset/create",
+            self._XmlBuild("changeset", {"tag": ChangesetTags})
+        )
+        self._CurrentChangesetId = int(result)
+        return self._CurrentChangesetId
+
+    def ChangesetClose(self):
+        """
+        Closes current changeset.
+
+        Returns `ChangesetId`.
+        """
+        if not self._CurrentChangesetId:
+            raise Exception("No changeset currently opened")
+        self._put(
+            "/api/0.6/changeset/"+str(self._CurrentChangesetId)+"/close",
+            ""
+        )
+        CurrentChangesetId = self._CurrentChangesetId
+        self._CurrentChangesetId = 0
+        return CurrentChangesetId
+
+    def ChangesetUpload(self, ChangesData):
+        """
+        Upload data with the `ChangesData` list of dicts:
+
+            #!python
+            {
+                type: node|way|relation,
+                action: create|delete|modify,
+                data: {}
+            }
+
+        Returns list with updated ids.
+        """
+        data = ""
+        data += "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
+        data += "<osmChange version=\"0.6\" generator=\""
+        data += self._created_by + "\">\n"
+        for change in ChangesData:
+            data += "<"+change["action"]+">\n"
+            change["data"]["changeset"] = self._CurrentChangesetId
+            data += self._XmlBuild(
+                change["type"],
+                change["data"],
+                False
+            ).decode("utf-8")
+            data += "</"+change["action"]+">\n"
+        data += "</osmChange>"
+        data = self._post(
+            "/api/0.6/changeset/"+str(self._CurrentChangesetId)+"/upload",
+            data.encode("utf-8")
+        )
+        data = xml.dom.minidom.parseString(data)
+        data = data.getElementsByTagName("diffResult")[0]
+        data = [x for x in data.childNodes if x.nodeType == x.ELEMENT_NODE]
+        for i in range(len(ChangesData)):
+            if ChangesData[i]["action"] == "delete":
+                ChangesData[i]["data"].pop("version")
+            else:
+                new_id = int(data[i].getAttribute("new_id"))
+                ChangesData[i]["data"]["id"] = new_id
+                new_version = int(data[i].getAttribute("new_version"))
+                ChangesData[i]["data"]["version"] = new_version
+        return ChangesData
+
+    def ChangesetDownload(self, ChangesetId):
+        """
+        Download data from changeset `ChangesetId`.
+
+        Returns list of dict:
+
+            #!python
+            {
+                'type': node|way|relation,
+                'action': create|delete|modify,
+                'data': {}
+            }
+        """
+        uri = "/api/0.6/changeset/"+str(ChangesetId)+"/download"
+        data = self._get(uri)
+        return self.ParseOsc(data)
+
+    def ChangesetsGet(  # noqa
+            self,
+            min_lon=None,
+            min_lat=None,
+            max_lon=None,
+            max_lat=None,
+            userid=None,
+            username=None,
+            closed_after=None,
+            created_before=None,
+            only_open=False,
+            only_closed=False):
+        """
+        Returns a dict with the id of the changeset as key
+        matching all criteria:
+
+            #!python
+            {
+                '1234': dict of ChangesetData,
+                '5678': dict of ChangesetData,
+                ...
+            }
+
+        All parameters are optional.
+        """
+
+        uri = "/api/0.6/changesets"
+        params = {}
+        if min_lon or min_lat or max_lon or max_lat:
+            params["bbox"] = ",".join(
+                [
+                    str(min_lon),
+                    str(min_lat),
+                    str(max_lon),
+                    str(max_lat)
+                ]
+            )
+        if userid:
+            params["user"] = userid
+        if username:
+            params["display_name"] = username
+        if closed_after and not created_before:
+            params["time"] = closed_after
+        if created_before:
+            if not closed_after:
+                closed_after = "1970-01-01T00:00:00Z"
+            params["time"] = closed_after + "," + created_before
+        if only_open:
+            params["open"] = 1
+        if only_closed:
+            params["closed"] = 1
+
+        if params:
+            uri += "?" + urllib.urlencode(params)
+
+        data = self._get(uri)
+        data = xml.dom.minidom.parseString(data)
+        data = data.getElementsByTagName("osm")[0]
+        data = data.getElementsByTagName("changeset")
+        result = {}
+        for curChangeset in data:
+            tmpCS = self._DomParseChangeset(curChangeset)
+            result[tmpCS["id"]] = tmpCS
+        return result
+
+    def ChangesetComment(self, ChangesetId, comment):
+        """
+        Adds a comment to the changeset `ChangesetId`
+
+        `comment` should be a string.
+
+        Returns the updated `ChangesetData` dict:
+
+            #!python
+            {
+                'id': id of Changeset,
+                'open': True|False, wheter or not this changeset is open
+                'tag': {} dict of tags,
+                'created_at': timestamp of creation of this changeset
+                'closed_at': timestamp when changeset was closed
+                'comments_count': amount of comments
+                'max_lon': maximum longitude of changes in this changeset
+                'max_lat': maximum latitude of changes in this changeset
+                'min_lon': minimum longitude of changes in this changeset
+                'min_lat': minimum longitude of changes in this changeset
+                'user': username of user that created this changeset,
+                'uid': id of user that created this changeset,
+            }
+
+        """
+        params = urllib.urlencode({'text': comment})
+        data = self._post(
+            "/api/0.6/changeset/"+str(ChangesetId)+"/comment",
+            params
+        )
+        data = xml.dom.minidom.parseString(data)
+        data = data.getElementsByTagName("osm")[0]
+        data = data.getElementsByTagName("changeset")[0]
+        return self._DomParseChangeset(data)
+
+    def ChangesetSubscribe(self, ChangesetId):
+        """
+        Subcribe to the changeset discussion of changeset `ChangesetId`.
+
+        The user will be informed about new comments (i.e. receive an email).
+
+        Returns the updated `ChangesetData` dict:
+
+            #!python
+            {
+                'id': id of Changeset,
+                'open': True|False, wheter or not this changeset is open
+                'tag': {} dict of tags,
+                'created_at': timestamp of creation of this changeset
+                'closed_at': timestamp when changeset was closed
+                'comments_count': amount of comments
+                'max_lon': maximum longitude of changes in this changeset
+                'max_lat': maximum latitude of changes in this changeset
+                'min_lon': minimum longitude of changes in this changeset
+                'min_lat': minimum longitude of changes in this changeset
+                'user': username of user that created this changeset,
+                'uid': id of user that created this changeset,
+            }
+        """
+        try:
+            data = self._post(
+                "/api/0.6/changeset/"+str(ChangesetId)+"/subscribe",
+                None
+            )
+        except ApiError as e:
+            if e.status == 409:
+                raise AlreadySubscribedApiError(e.status, e.reason, e.payload)
+            else:
+                raise
+        data = xml.dom.minidom.parseString(data)
+        data = data.getElementsByTagName("osm")[0]
+        data = data.getElementsByTagName("changeset")[0]
+        return self._DomParseChangeset(data)
+
+    def ChangesetUnsubscribe(self, ChangesetId):
+        """
+        Subcribe to the changeset discussion of changeset `ChangesetId`.
+
+        The user will be informed about new comments (i.e. receive an email).
+
+        Returns the updated `ChangesetData` dict:
+
+            #!python
+            {
+                'id': id of Changeset,
+                'open': True|False, wheter or not this changeset is open
+                'tag': {} dict of tags,
+                'created_at': timestamp of creation of this changeset
+                'closed_at': timestamp when changeset was closed
+                'comments_count': amount of comments
+                'max_lon': maximum longitude of changes in this changeset
+                'max_lat': maximum latitude of changes in this changeset
+                'min_lon': minimum longitude of changes in this changeset
+                'min_lat': minimum longitude of changes in this changeset
+                'user': username of user that created this changeset,
+                'uid': id of user that created this changeset,
+            }
+        """
+        try:
+            data = self._post(
+                "/api/0.6/changeset/"+str(ChangesetId)+"/unsubscribe",
+                None
+            )
+        except ApiError as e:
+            if e.status == 404:
+                raise NotSubscribedApiError(e.status, e.reason, e.payload)
+            else:
+                raise
+        data = xml.dom.minidom.parseString(data)
+        data = data.getElementsByTagName("osm")[0]
+        data = data.getElementsByTagName("changeset")[0]
+        return self._DomParseChangeset(data)
+
+    ##################################################
+    # Notes                                          #
+    ##################################################
+
+    def NotesGet(
+            self,
+            min_lon,
+            min_lat,
+            max_lon,
+            max_lat,
+            limit=100,
+            closed=7):
+        """
+        Returns a list of dicts of notes in the specified bounding box:
+
+            #!python
+            [
+                {
+                    'id': integer,
+                    'action': opened|commented|closed,
+                    'status': open|closed
+                    'date_created': creation date
+                    'date_closed': closing data|None
+                    'uid': User ID|None
+                    'user': User name|None
+                    'comments': {}
+                },
+                { ... }
+            ]
+
+        The limit parameter defines how many results should be returned.
+
+        closed specifies the number of days a bug needs to be closed
+        to no longer be returned.
+        The value 0 means only open bugs are returned,
+        -1 means all bugs are returned.
+
+        All parameters are optional.
+        """
+        uri = (
+            "/api/0.6/notes?bbox=%f,%f,%f,%f&limit=%d&closed=%d"
+            % (min_lon, min_lat, max_lon, max_lat, limit, closed)
+        )
+        data = self._get(uri)
+        return self.ParseNotes(data)
+
+    def NoteGet(self, id):
+        """
+        Returns a note as dict:
+
+            #!python
+            {
+                'id': integer,
+                'action': opened|commented|closed,
+                'status': open|closed
+                'date_created': creation date
+                'date_closed': closing data|None
+                'uid': User ID|None
+                'user': User name|None
+                'comments': {}
+            }
+
+        `id` is the unique identifier of the note.
+        """
+        uri = "/api/0.6/notes/%s" % (id)
+        data = self._get(uri)
+        data = xml.dom.minidom.parseString(data)
+        osm_data = data.getElementsByTagName("osm")[0]
+
+        noteElement = osm_data.getElementsByTagName("note")[0]
+        note = self._DomParseNote(noteElement)
+        return note
+
+    def NoteCreate(self, NoteData):
+        """
+        Creates a note.
+        Returns updated NoteData (without timestamp).
+        """
+        uri = "/api/0.6/notes"
+        uri += "?" + urllib.urlencode(NoteData)
+        return self._NoteAction(uri)
+
+    def NoteComment(self, NoteId, comment):
+        """
+        Adds a new comment to a note.
+        Returns the updated note.
+        """
+        path = "/api/0.6/notes/%s/comment" % NoteId
+        return self._NoteAction(path, comment)
+
+    def NoteClose(self, NoteId, comment):
+        """
+        Closes a note.
+        Returns the updated note.
+        """
+        path = "/api/0.6/notes/%s/close" % NoteId
+        return self._NoteAction(path, comment, optionalAuth=False)
+
+    def NoteReopen(self, NoteId, comment):
+        """
+        Reopens a note.
+        Returns the updated note.
+        """
+        path = "/api/0.6/notes/%s/reopen" % NoteId
+        return self._NoteAction(path, comment, optionalAuth=False)
+
+    def NotesSearch(self, query, limit=100, closed=7):
+        """
+        Returns a list of dicts of notes that match the given search query.
+
+        The limit parameter defines how many results should be returned.
+
+        closed specifies the number of days a bug needs to be closed
+        to no longer be returned.
+        The value 0 means only open bugs are returned,
+        -1 means all bugs are returned.
+        """
+        uri = "/api/0.6/notes/search"
+        params = {}
+        params['q'] = query
+        params['limit'] = limit
+        params['closed'] = closed
+        uri += "?" + urllib.urlencode(params)
+        data = self._get(uri)
+
+        return self.ParseNotes(data)
+
+    def _NoteAction(self, path, comment=None, optionalAuth=True):
+        """
+        Performs an action on a Note with a comment
+        Return the updated note
+        """
+        uri = path
+        if comment is not None:
+            params = {}
+            params['text'] = comment
+            uri += "?" + urllib.urlencode(params)
+        result = self._post(uri, None, optionalAuth=optionalAuth)
+
+        # parse the result
+        data = xml.dom.minidom.parseString(result)
+        osm_data = data.getElementsByTagName("osm")[0]
+
+        noteElement = osm_data.getElementsByTagName("note")[0]
+        note = self._DomParseNote(noteElement)
+
+        return note
+
+    ##################################################
+    # Other                                          #
+    ##################################################
+
+    def Map(self, min_lon, min_lat, max_lon, max_lat):
+        """
+        Download data in bounding box.
+
+        Returns list of dict:
+
+            #!python
+            {
+                type: node|way|relation,
+                data: {}
+            }
+        """
+        uri = (
+            "/api/0.6/map?bbox=%f,%f,%f,%f"
+            % (min_lon, min_lat, max_lon, max_lat)
+        )
+        data = self._get(uri)
+        return self.ParseOsm(data)
+
+    ##################################################
+    # Data parser                                    #
+    ##################################################
+
+    def ParseOsm(self, data):
+        """
+        Parse osm data.
+
+        Returns list of dict:
+
+            #!python
+            {
+                type: node|way|relation,
+                data: {}
+            }
+        """
+        data = xml.dom.minidom.parseString(data)
+        data = data.getElementsByTagName("osm")[0]
+        result = []
+        for elem in data.childNodes:
+            if elem.nodeName == "node":
+                result.append({
+                    "type": elem.nodeName,
+                    "data": self._DomParseNode(elem)
+                })
+            elif elem.nodeName == "way":
+                result.append({
+                    "type": elem.nodeName,
+                    "data": self._DomParseWay(elem)
+                })
+            elif elem.nodeName == "relation":
+                result.append({
+                    "type": elem.nodeName,
+                    "data": self._DomParseRelation(elem)
+                })
+        return result
+
+    def ParseOsc(self, data):
+        """
+        Parse osc data.
+
+        Returns list of dict:
+
+            #!python
+            {
+                type: node|way|relation,
+                action: create|delete|modify,
+                data: {}
+            }
+        """
+        data = xml.dom.minidom.parseString(data)
+        data = data.getElementsByTagName("osmChange")[0]
+        result = []
+        for action in data.childNodes:
+            if action.nodeName == "#text":
+                continue
+            for elem in action.childNodes:
+                if elem.nodeName == "node":
+                    result.append({
+                        "action": action.nodeName,
+                        "type": elem.nodeName,
+                        "data": self._DomParseNode(elem)
+                    })
+                elif elem.nodeName == "way":
+                    result.append({
+                        "action": action.nodeName,
+                        "type": elem.nodeName,
+                        "data": self._DomParseWay(elem)
+                    })
+                elif elem.nodeName == "relation":
+                    result.append({
+                        "action": action.nodeName,
+                        "type": elem.nodeName,
+                        "data": self._DomParseRelation(elem)
+                    })
+        return result
+
+    def ParseNotes(self, data):
+        """
+        Parse notes data.
+
+        Returns a list of dict:
+
+            #!python
+            [
+                {
+                    'id': integer,
+                    'action': opened|commented|closed,
+                    'status': open|closed
+                    'date_created': creation date
+                    'date_closed': closing data|None
+                    'uid': User ID|None
+                    'user': User name|None
+                    'comments': {}
+                },
+                { ... }
+            ]
+        """
+        data = xml.dom.minidom.parseString(data)
+        result = []
+        osm_data = data.getElementsByTagName("osm")[0]
+
+        for noteElement in osm_data.getElementsByTagName("note"):
+            note = self._DomParseNote(noteElement)
+            result.append(note)
+        return result
+
+    ##################################################
+    # Internal http function                         #
+    ##################################################
+
+    def _do(self, action, OsmType, OsmData):
+        if self._changesetauto:
+            self._changesetautodata.append({
+                "action": action,
+                "type": OsmType,
+                "data": OsmData
+            })
+            self._changesetautoflush()
+            return None
+        else:
+            return self._do_manu(action, OsmType, OsmData)
+
+    def _do_manu(self, action, OsmType, OsmData):
+        if not self._CurrentChangesetId:
+            raise Exception(
+                "You need to open a changeset before uploading data"
+            )
+        if "timestamp" in OsmData:
+            OsmData.pop("timestamp")
+        OsmData["changeset"] = self._CurrentChangesetId
+        if action == "create":
+            if OsmData.get("id", -1) > 0:
+                raise Exception("This "+OsmType+" already exists")
+            result = self._put(
+                "/api/0.6/" + OsmType + "/create",
+                self._XmlBuild(OsmType, OsmData)
+            )
+            OsmData["id"] = int(result.strip())
+            OsmData["version"] = 1
+            return OsmData
+        elif action == "modify":
+            result = self._put(
+                "/api/0.6/" + OsmType + "/" + str(OsmData["id"]),
+                self._XmlBuild(OsmType, OsmData)
+            )
+            OsmData["version"] = int(result.strip())
+            return OsmData
+        elif action == "delete":
+            result = self._delete(
+                "/api/0.6/" + OsmType + "/" + str(OsmData["id"]),
+                self._XmlBuild(OsmType, OsmData)
+            )
+            OsmData["version"] = int(result.strip())
+            OsmData["visible"] = False
+            return OsmData
+
+    def flush(self):
+        """
+        Force the changes to be uploaded to OSM and the changeset to be closed
+        """
+        return self._changesetautoflush(True)
+
+    def _changesetautoflush(self, force=False):
+        autosize = self._changesetautosize
+        while ((len(self._changesetautodata) >= autosize) or
+                (force and self._changesetautodata)):
+            if self._changesetautocpt == 0:
+                self.ChangesetCreate(self._changesetautotags)
+            self.ChangesetUpload(
+                self._changesetautodata[:autosize]
+            )
+            self._changesetautodata = self._changesetautodata[autosize:]
+            self._changesetautocpt += 1
+            if self._changesetautocpt == self._changesetautomulti:
+                self.ChangesetClose()
+                self._changesetautocpt = 0
+        if self._changesetautocpt and force:
+            self.ChangesetClose()
+            self._changesetautocpt = 0
+        return None
+
+    def _http_request(self, cmd, path, auth, send):  # noqa
+        if self._debug:
+            path2 = path
+            if len(path2) > 50:
+                path2 = path2[:50]+"[...]"
+            error_msg = (
+                "%s %s %s"
+                % (time.strftime("%Y-%m-%d %H:%M:%S"), cmd, path2)
+            )
+            print(error_msg, file=sys.stderr)
+        self._conn.putrequest(cmd, path)
+        self._conn.putheader('User-Agent', self._created_by)
+        if auth:
+            try:
+                user_pass = self._username + ':' + self._password
+            except AttributeError:
+                raise UsernamePasswordMissingError("Username/Password missing")
+
+            try:
+                # Python 2
+                base64_user_pass = base64.encodestring(user_pass).strip()
+            except TypeError:
+                # Python 3
+                base64_user_pass = base64.encodestring(
+                    user_pass.encode('ascii')
+                    ).strip()
+                base64_user_pass = base64_user_pass.decode('utf-8')
+
+            self._conn.putheader(
+                'Authorization',
+                'Basic ' + base64_user_pass
+            )
+        if send is not None:
+            self._conn.putheader('Content-Length', len(send))
+        self._conn.endheaders()
+        if send:
+            self._conn.send(send)
+        response = self._conn.getresponse()
+        if response.status != 200:
+            payload = response.read().strip()
+            if response.status == 410:
+                return None
+            raise ApiError(response.status, response.reason, payload)
+        if self._debug:
+            error_msg = (
+                "%s %s %s"
+                % (time.strftime("%Y-%m-%d %H:%M:%S"), cmd, path2)
+            )
+            print(error_msg, file=sys.stderr)
+        return response.read()
+
+    def _http(self, cmd, path, auth, send):  # noqa
+        i = 0
+        while True:
+            i += 1
+            try:
+                return self._http_request(cmd, path, auth, send)
+            except ApiError as e:
+                if e.status >= 500:
+                    if i == 5:
+                        raise
+                    if i != 1:
+                        self._sleep()
+                    self._conn = self._get_http_connection()
+                else:
+                    raise
+            except Exception:
+                if i == 5:
+                    raise
+                if i != 1:
+                    self._sleep()
+                self._conn = self._get_http_connection()
+
+    def _get_http_connection(self):
+        return httplib.HTTPConnection(self._api, 80)
+
+    def _sleep(self):
+        time.sleep(5)
+
+    def _get(self, path):
+        return self._http('GET', path, False, None)
+
+    def _put(self, path, data):
+        return self._http('PUT', path, True, data)
+
+    def _post(self, path, data, optionalAuth=False):
+        auth = True
+        # the Notes API allows certain POSTs by non-authenticated users
+        if optionalAuth:
+            auth = hasattr(self, '_username')
+        return self._http('POST', path, auth, data)
+
+    def _delete(self, path, data):
+        return self._http('DELETE', path, True, data)
+
+    ##################################################
+    # Internal dom function                          #
+    ##################################################
+
+    def _DomGetAttributes(self, DomElement):  # noqa
+        """
+        Returns a formated dictionnary of attributes of a DomElement.
+        """
+        result = {}
+        for k, v in DomElement.attributes.items():
+            if k == "uid":
+                v = int(v)
+            elif k == "changeset":
+                v = int(v)
+            elif k == "version":
+                v = int(v)
+            elif k == "id":
+                v = int(v)
+            elif k == "lat":
+                v = float(v)
+            elif k == "lon":
+                v = float(v)
+            elif k == "open":
+                v = (v == "true")
+            elif k == "visible":
+                v = (v == "true")
+            elif k == "ref":
+                v = int(v)
+            elif k == "comments_count":
+                v = int(v)
+            elif k == "timestamp":
+                v = self._ParseDate(v)
+            elif k == "created_at":
+                v = self._ParseDate(v)
+            elif k == "closed_at":
+                v = self._ParseDate(v)
+            elif k == "date":
+                v = self._ParseDate(v)
+            result[k] = v
+        return result
+
+    def _DomGetTag(self, DomElement):
+        """
+        Returns the dictionnary of tags of a DomElement.
+        """
+        result = {}
+        for t in DomElement.getElementsByTagName("tag"):
+            k = t.attributes["k"].value
+            v = t.attributes["v"].value
+            result[k] = v
+        return result
+
+    def _DomGetNd(self, DomElement):
+        """
+        Returns the list of nodes of a DomElement.
+        """
+        result = []
+        for t in DomElement.getElementsByTagName("nd"):
+            result.append(int(int(t.attributes["ref"].value)))
+        return result
+
+    def _DomGetDiscussion(self, DomElement):
+        """
+        Returns the dictionnary of comments of a DomElement.
+        """
+        result = []
+        try:
+            discussion = DomElement.getElementsByTagName("discussion")[0]
+            for t in discussion.getElementsByTagName("comment"):
+                comment = self._DomGetAttributes(t)
+                comment['text'] = self._GetXmlValue(t, "text")
+                result.append(comment)
+        except IndexError:
+            pass
+        return result
+
+    def _DomGetComments(self, DomElement):
+        """
+        Returns the list of comments of a DomElement.
+        """
+        result = []
+        for t in DomElement.getElementsByTagName("comment"):
+            comment = {}
+            comment['date'] = self._ParseDate(self._GetXmlValue(t, "date"))
+            comment['action'] = self._GetXmlValue(t, "action")
+            comment['text'] = self._GetXmlValue(t, "text")
+            comment['html'] = self._GetXmlValue(t, "html")
+            comment['uid'] = self._GetXmlValue(t, "uid")
+            comment['user'] = self._GetXmlValue(t, "user")
+            result.append(comment)
+        return result
+
+    def _DomGetMember(self, DomElement):
+        """
+        Returns a list of relation members.
+        """
+        result = []
+        for m in DomElement.getElementsByTagName("member"):
+            result.append(self._DomGetAttributes(m))
+        return result
+
+    def _DomParseNode(self, DomElement):
+        """
+        Returns NodeData for the node.
+        """
+        result = self._DomGetAttributes(DomElement)
+        result["tag"] = self._DomGetTag(DomElement)
+        return result
+
+    def _DomParseWay(self, DomElement):
+        """
+        Returns WayData for the way.
+        """
+        result = self._DomGetAttributes(DomElement)
+        result["tag"] = self._DomGetTag(DomElement)
+        result["nd"] = self._DomGetNd(DomElement)
+        return result
+
+    def _DomParseRelation(self, DomElement):
+        """
+        Returns RelationData for the relation.
+        """
+        result = self._DomGetAttributes(DomElement)
+        result["tag"] = self._DomGetTag(DomElement)
+        result["member"] = self._DomGetMember(DomElement)
+        return result
+
+    def _DomParseChangeset(self, DomElement):
+        """
+        Returns ChangesetData for the changeset.
+        """
+        result = self._DomGetAttributes(DomElement)
+        result["tag"] = self._DomGetTag(DomElement)
+        result["discussion"] = self._DomGetDiscussion(DomElement)
+
+        return result
+
+    def _DomParseNote(self, DomElement):
+        """
+        Returns NoteData for the note.
+        """
+        result = self._DomGetAttributes(DomElement)
+        result["id"] = self._GetXmlValue(DomElement, "id")
+        result["status"] = self._GetXmlValue(DomElement, "status")
+
+        result["date_created"] = self._ParseDate(
+            self._GetXmlValue(DomElement, "date_created")
+        )
+        result["date_closed"] = self._ParseDate(
+            self._GetXmlValue(DomElement, "date_closed")
+        )
+        result["comments"] = self._DomGetComments(DomElement)
+
+        return result
+
+    def _ParseDate(self, DateString):
+        result = DateString
+        try:
+            result = datetime.strptime(DateString, "%Y-%m-%d %H:%M:%S UTC")
+        except:
+            try:
+                result = datetime.strptime(DateString, "%Y-%m-%dT%H:%M:%SZ")
+            except:
+                pass
+
+        return result
+
+    ##################################################
+    # Internal xml builder                           #
+    ##################################################
+
+    def _XmlBuild(self, ElementType, ElementData, WithHeaders=True):  # noqa
+
+        xml = ""
+        if WithHeaders:
+            xml += "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
+            xml += "<osm version=\"0.6\" generator=\""
+            xml += self._created_by + "\">\n"
+
+        # <element attr="val">
+        xml += "  <" + ElementType
+        if "id" in ElementData:
+            xml += " id=\"" + str(ElementData["id"]) + "\""
+        if "lat" in ElementData:
+            xml += " lat=\"" + str(ElementData["lat"]) + "\""
+        if "lon" in ElementData:
+            xml += " lon=\"" + str(ElementData["lon"]) + "\""
+        if "version" in ElementData:
+            xml += " version=\"" + str(ElementData["version"]) + "\""
+        visible_str = str(ElementData.get("visible", True)).lower()
+        xml += " visible=\"" + visible_str + "\""
+        if ElementType in ["node", "way", "relation"]:
+            xml += " changeset=\"" + str(self._CurrentChangesetId) + "\""
+        xml += ">\n"
+
+        # <tag... />
+        for k, v in ElementData.get("tag", {}).items():
+            xml += "    <tag k=\"" + self._XmlEncode(k)
+            xml += "\" v=\"" + self._XmlEncode(v)+"\"/>\n"
+
+        # <member... />
+        for member in ElementData.get("member", []):
+            xml += "    <member type=\"" + member["type"]
+            xml += "\" ref=\"" + str(member["ref"])
+            xml += "\" role=\"" + self._XmlEncode(member["role"])
+            xml += "\"/>\n"
+
+        # <nd... />
+        for ref in ElementData.get("nd", []):
+            xml += "    <nd ref=\""+str(ref)+"\"/>\n"
+
+        # </element>
+        xml += "  </" + ElementType + ">\n"
+
+        if WithHeaders:
+            xml += "</osm>\n"
+
+        return xml.encode("utf8")
+
+    def _XmlEncode(self, text):
+        return (
+            text
+            .replace("&", "&")
+            .replace("\"", """)
+            .replace("<", "<")
+            .replace(">", ">")
+        )
+
+    def _GetXmlValue(self, DomElement, tag):
+        try:
+            elem = DomElement.getElementsByTagName(tag)[0]
+            return elem.firstChild.nodeValue
+        except:
+            return None
diff --git a/osmapi/__init__.py b/osmapi/__init__.py
new file mode 100644
index 0000000..ddb4e18
--- /dev/null
+++ b/osmapi/__init__.py
@@ -0,0 +1,5 @@
+from __future__ import (absolute_import, print_function, unicode_literals)
+
+__version__ = '0.5.0'
+
+from .OsmApi import *  # noqa
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..1842bac
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,5 @@
+# This file lists the dependencies of this extension.
+# Install with a command like: pip install -r pip-requirements.txt
+pypandoc==0.7.0
+Unidecode==0.04.14
+pdoc==0.3.1
diff --git a/setup.cfg b/setup.cfg
new file mode 100644
index 0000000..fc4fde0
--- /dev/null
+++ b/setup.cfg
@@ -0,0 +1,5 @@
+[metadata]
+description-file = README.md
+
+[flake8]
+max-complexity = 10
diff --git a/setup.py b/setup.py
new file mode 100644
index 0000000..dd70272
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,44 @@
+# -*- coding: utf-8 -*-
+
+import codecs
+
+version = __import__('osmapi').__version__
+
+try:
+    import pypandoc
+    from unidecode import unidecode
+    description = codecs.open('README.md', encoding='utf-8').read()
+    description = unidecode(description)
+    description = pypandoc.convert(description, 'rst', format='md')
+except (IOError, ImportError):
+    description = 'Python wrapper for the OSM API'
+
+from distutils.core import setup
+setup(
+    name='osmapi',
+    packages=['osmapi'],
+    version=version,
+    description='Python wrapper for the OSM API',
+    long_description=description,
+    author='Etienne Chové',
+    author_email='chove at crans.org',
+    maintainer='Stefan Oderbolz',
+    maintainer_email='odi at readmore.ch',
+    url='https://github.com/metaodi/osmapi',
+    download_url='https://github.com/metaodi/osmapi/archive/v%s.zip' % version,
+    keywords=['openstreetmap', 'osm', 'api'],
+    license='GPLv3',
+    classifiers=[
+        'License :: OSI Approved :: GNU General Public License v3 (GPLv3)',
+        'Intended Audience :: Developers',
+        'Topic :: Scientific/Engineering :: GIS',
+        'Topic :: Software Development :: Libraries',
+        'Development Status :: 4 - Beta',
+        'Programming Language :: Python :: 2',
+        'Programming Language :: Python :: 2.6',
+        'Programming Language :: Python :: 2.7',
+        'Programming Language :: Python :: 3',
+        'Programming Language :: Python :: 3.2',
+        'Programming Language :: Python :: 3.3',
+    ],
+)
diff --git a/test-requirements.txt b/test-requirements.txt
new file mode 100644
index 0000000..48ee360
--- /dev/null
+++ b/test-requirements.txt
@@ -0,0 +1,8 @@
+# This file lists the dependencies of this extension.
+# Install with a command like: pip install -r pip-requirements.txt
+flake8==2.1.0
+nose==1.3.0
+tox==1.7.1
+coveralls==0.4.1
+mock==1.0.1
+xmltodict==0.9.0
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/capabilities_test.py b/tests/capabilities_test.py
new file mode 100644
index 0000000..79b029e
--- /dev/null
+++ b/tests/capabilities_test.py
@@ -0,0 +1,23 @@
+from __future__ import (unicode_literals, absolute_import)
+from nose.tools import *  # noqa
+from . import osmapi_tests
+
+
+class TestOsmApiNode(osmapi_tests.TestOsmApi):
+    def test_Capabilities(self):
+        self._conn_mock()
+
+        result = self.api.Capabilities()
+        assert_equals(result, {
+            'area': {'maximum': 0.25},
+            'changesets': {'maximum_elements': 50000.0},
+            'status': {
+                'api': 'mocked',
+                'database': 'online',
+                'gpx': 'online'
+            },
+            'timeout': {'seconds': 300.0},
+            'tracepoints': {'per_page': 5000.0},
+            'version': {'maximum': 0.6, 'minimum': 0.6},
+            'waynodes': {'maximum': 2000.0}
+        })
diff --git a/tests/changeset_tests.py b/tests/changeset_tests.py
new file mode 100644
index 0000000..7024a23
--- /dev/null
+++ b/tests/changeset_tests.py
@@ -0,0 +1,700 @@
+from __future__ import (unicode_literals, absolute_import)
+from nose.tools import *  # noqa
+from . import osmapi_tests
+from osmapi import AlreadySubscribedApiError, NotSubscribedApiError
+import mock
+import xmltodict
+import datetime
+try:
+    import urlparse
+except:
+    import urllib
+    urlparse = urllib.parse
+
+
+def recursive_sort(col):  # noqa
+    """
+    Function to recursive sort a collection
+    that might contain lists, dicts etc.
+    In Python 3.x a list of dicts is sorted by it's hash
+    """
+    if hasattr(col, '__iter__'):
+        if isinstance(col, list):
+            try:
+                col = sorted(col)
+            except TypeError:  # in Python 3.x: lists of dicts are not sortable
+                col = sorted(col, key=lambda k: hash(frozenset(k.items())))
+            except:
+                pass
+
+            for idx, elem in enumerate(col):
+                col[idx] = recursive_sort(elem)
+        elif isinstance(col, dict):
+            for elem in col:
+                try:
+                    col[elem] = recursive_sort(col[elem])
+                except IndexError:
+                    pass
+    return col
+
+
+def xmltosorteddict(xml):
+    xml_dict = xmltodict.parse(xml, dict_constructor=dict)
+    return recursive_sort(xml_dict)
+
+
+def debug(result):
+    from pprint import pprint
+    pprint(result)
+    assert_equals(0, 1)
+
+
+class TestOsmApiChangeset(osmapi_tests.TestOsmApi):
+    def test_ChangesetGet(self):
+        self._conn_mock()
+
+        result = self.api.ChangesetGet(123)
+
+        args, kwargs = self.api._conn.putrequest.call_args
+        self.assertEquals(args[0], 'GET')
+        self.assertEquals(args[1], '/api/0.6/changeset/123')
+
+        self.assertEquals(result, {
+            'id': 123,
+            'closed_at': datetime.datetime(2009, 9, 7, 22, 57, 37),
+            'created_at': datetime.datetime(2009, 9, 7, 21, 57, 36),
+            'discussion': [],
+            'max_lat': '52.4710193',
+            'max_lon': '-1.4831815',
+            'min_lat': '45.9667901',
+            'min_lon': '-1.4998534',
+            'open': False,
+            'user': 'randomjunk',
+            'uid': 3,
+            'tag': {
+                'comment': 'correct node bug',
+                'created_by': 'Potlatch 1.2a',
+            },
+        })
+
+    def test_ChangesetUpdate(self):
+        self._conn_mock(auth=True)
+
+        # setup mock
+        self.api.ChangesetCreate = mock.Mock(
+            return_value=4444
+        )
+        self.api._CurrentChangesetId = 4444
+
+        result = self.api.ChangesetUpdate(
+            {
+                'test': 'foobar'
+            }
+        )
+
+        args, _ = self.api._conn.putrequest.call_args
+        self.assertEquals(args[0], 'PUT')
+        self.assertEquals(args[1], '/api/0.6/changeset/4444')
+        sendargs, _ = self.api._conn.send.call_args
+        self.assertEquals(
+            xmltosorteddict(sendargs[0]),
+            xmltosorteddict(
+                b'<?xml version="1.0" encoding="UTF-8"?>\n'
+                b'<osm version="0.6" generator="osmapi/0.5.0">\n'
+                b'  <changeset visible="true">\n'
+                b'    <tag k="test" v="foobar"/>\n'
+                b'    <tag k="created_by" v="osmapi/0.5.0"/>\n'
+                b'  </changeset>\n'
+                b'</osm>\n'
+            )
+        )
+        self.assertEquals(result, 4444)
+
+    def test_ChangesetUpdate_with_created_by(self):
+        self._conn_mock(auth=True)
+
+        # setup mock
+        self.api.ChangesetCreate = mock.Mock(
+            return_value=4444
+        )
+        self.api._CurrentChangesetId = 4444
+
+        result = self.api.ChangesetUpdate(
+            {
+                'test': 'foobar',
+                'created_by': 'MyTestOSMApp'
+            }
+        )
+
+        args, _ = self.api._conn.putrequest.call_args
+        self.assertEquals(args[0], 'PUT')
+        self.assertEquals(args[1], '/api/0.6/changeset/4444')
+        sendargs, _ = self.api._conn.send.call_args
+        self.assertEquals(
+            xmltosorteddict(sendargs[0]),
+            xmltosorteddict(
+                b'<?xml version="1.0" encoding="UTF-8"?>\n'
+                b'<osm version="0.6" generator="osmapi/0.5.0">\n'
+                b'  <changeset visible="true">\n'
+                b'    <tag k="test" v="foobar"/>\n'
+                b'    <tag k="created_by" v="MyTestOSMApp"/>\n'
+                b'  </changeset>\n'
+                b'</osm>\n'
+            )
+        )
+        self.assertEquals(result, 4444)
+
+    def test_ChangesetUpdate_wo_changeset(self):
+        self._conn_mock()
+
+        with self.assertRaisesRegexp(
+                Exception,
+                'No changeset currently opened'):
+            self.api.ChangesetUpdate(
+                {
+                    'test': 'foobar'
+                }
+            )
+
+    def test_ChangesetCreate(self):
+        self._conn_mock(auth=True)
+
+        result = self.api.ChangesetCreate(
+            {
+                'foobar': 'A new test changeset'
+            }
+        )
+
+        args, kwargs = self.api._conn.putrequest.call_args
+        self.assertEquals(args[0], 'PUT')
+        self.assertEquals(args[1], '/api/0.6/changeset/create')
+        sendargs, kwargs = self.api._conn.send.call_args
+        self.assertEquals(
+            xmltosorteddict(sendargs[0]),
+            xmltosorteddict(
+                b'<?xml version="1.0" encoding="UTF-8"?>\n'
+                b'<osm version="0.6" generator="osmapi/0.5.0">\n'
+                b'  <changeset visible="true">\n'
+                b'    <tag k="foobar" v="A new test changeset"/>\n'
+                b'    <tag k="created_by" v="osmapi/0.5.0"/>\n'
+                b'  </changeset>\n'
+                b'</osm>\n'
+            )
+        )
+        self.assertEquals(result, 4321)
+
+    def test_ChangesetCreate_with_created_by(self):
+        self._conn_mock(auth=True)
+
+        result = self.api.ChangesetCreate(
+            {
+                'foobar': 'A new test changeset',
+                'created_by': 'CoolTestApp',
+            }
+        )
+
+        args, _ = self.api._conn.putrequest.call_args
+        self.assertEquals(args[0], 'PUT')
+        self.assertEquals(args[1], '/api/0.6/changeset/create')
+        sendargs, _ = self.api._conn.send.call_args
+        self.assertEquals(
+            xmltosorteddict(sendargs[0]),
+            xmltosorteddict(
+                b'<?xml version="1.0" encoding="UTF-8"?>\n'
+                b'<osm version="0.6" generator="osmapi/0.5.0">\n'
+                b'  <changeset visible="true">\n'
+                b'    <tag k="foobar" v="A new test changeset"/>\n'
+                b'    <tag k="created_by" v="CoolTestApp"/>\n'
+                b'  </changeset>\n'
+                b'</osm>\n'
+            )
+        )
+        self.assertEquals(result, 1234)
+
+    def test_ChangesetCreate_with_open_changeset(self):
+        self._conn_mock(auth=True)
+
+        self.api.ChangesetCreate(
+            {
+                'test': 'an already open changeset',
+            }
+        )
+
+        with self.assertRaisesRegexp(
+                Exception,
+                'Changeset already opened'):
+            self.api.ChangesetCreate(
+                {
+                    'test': 'foobar'
+                }
+            )
+
+    def test_ChangesetClose(self):
+        self._conn_mock(auth=True)
+
+        # setup mock
+        self.api.ChangesetCreate = mock.Mock(
+            return_value=4444
+        )
+        self.api._CurrentChangesetId = 4444
+
+        self.api.ChangesetClose()
+
+        args, kwargs = self.api._conn.putrequest.call_args
+        self.assertEquals(args[0], 'PUT')
+        self.assertEquals(args[1], '/api/0.6/changeset/4444/close')
+
+    def test_ChangesetClose_with_no_changeset(self):
+        self._conn_mock()
+
+        with self.assertRaisesRegexp(
+                Exception,
+                'No changeset currently opened'):
+            self.api.ChangesetClose()
+
+    def test_ChangesetUpload_create_node(self):
+        self._conn_mock(auth=True)
+
+        # setup mock
+        self.api.ChangesetCreate = mock.Mock(
+            return_value=4444
+        )
+        self.api._CurrentChangesetId = 4444
+
+        changesdata = [
+            {
+                'type': 'node',
+                'action': 'create',
+                'data': {
+                    'lat': 47.123,
+                    'lon': 8.555,
+                    'tag': {
+                        'amenity': 'place_of_worship',
+                        'religion': 'pastafarian'
+                    }
+                }
+            }
+        ]
+
+        result = self.api.ChangesetUpload(changesdata)
+
+        args, _ = self.api._conn.putrequest.call_args
+        self.assertEquals(args[0], 'POST')
+        self.assertEquals(args[1], '/api/0.6/changeset/4444/upload')
+        sendargs, _ = self.api._conn.send.call_args
+        self.assertEquals(
+            xmltosorteddict(sendargs[0]),
+            xmltosorteddict(
+                b'<?xml version="1.0" encoding="UTF-8"?>\n'
+                b'<osmChange version="0.6" generator="osmapi/0.5.0">\n'
+                b'<create>\n'
+                b'  <node lat="47.123" lon="8.555" visible="true" '
+                b'changeset="4444">\n'
+                b'    <tag k="religion" v="pastafarian"/>\n'
+                b'    <tag k="amenity" v="place_of_worship"/>\n'
+                b'  </node>\n'
+                b'</create>\n'
+                b'</osmChange>'
+            )
+        )
+
+        self.assertEquals(result[0]['type'], changesdata[0]['type'])
+        self.assertEquals(result[0]['action'], changesdata[0]['action'])
+
+        data = result[0]['data']
+        self.assertEquals(data['lat'], changesdata[0]['data']['lat'])
+        self.assertEquals(data['lon'], changesdata[0]['data']['lon'])
+        self.assertEquals(data['tag'], changesdata[0]['data']['tag'])
+        self.assertEquals(data['id'], 4295832900)
+        self.assertEquals(result[0]['data']['version'], 1)
+
+    def test_ChangesetUpload_modify_way(self):
+        self._conn_mock(auth=True)
+
+        # setup mock
+        self.api.ChangesetCreate = mock.Mock(
+            return_value=4444
+        )
+        self.api._CurrentChangesetId = 4444
+
+        changesdata = [
+            {
+                'type': 'way',
+                'action': 'modify',
+                'data': {
+                    'id': 4294967296,
+                    'version': 2,
+                    'nd': [
+                        4295832773,
+                        4295832773,
+                        4294967304,
+                        4294967303,
+                        4294967300,
+                        4608751,
+                        4294967305,
+                        4294967302,
+                        8548430,
+                        4294967296,
+                        4294967301,
+                        4294967298,
+                        4294967306,
+                        7855737,
+                        4294967297,
+                        4294967299
+                    ],
+                    'tag': {
+                        'highway': 'secondary',
+                        'name': 'Stansted Road'
+                    }
+                }
+            }
+        ]
+
+        result = self.api.ChangesetUpload(changesdata)
+
+        args, _ = self.api._conn.putrequest.call_args
+        self.assertEquals(args[0], 'POST')
+        self.assertEquals(args[1], '/api/0.6/changeset/4444/upload')
+        sendargs, _ = self.api._conn.send.call_args
+        self.assertEquals(
+            xmltosorteddict(sendargs[0]),
+            xmltosorteddict(
+                b'<?xml version="1.0" encoding="UTF-8"?>\n'
+                b'<osmChange version="0.6" generator="osmapi/0.5.0">\n'
+                b'<modify>\n'
+                b'  <way id="4294967296" version="2" visible="true" '
+                b'changeset="4444">\n'
+                b'    <tag k="name" v="Stansted Road"/>\n'
+                b'    <tag k="highway" v="secondary"/>\n'
+                b'    <nd ref="4295832773"/>\n'
+                b'    <nd ref="4295832773"/>\n'
+                b'    <nd ref="4294967304"/>\n'
+                b'    <nd ref="4294967303"/>\n'
+                b'    <nd ref="4294967300"/>\n'
+                b'    <nd ref="4608751"/>\n'
+                b'    <nd ref="4294967305"/>\n'
+                b'    <nd ref="4294967302"/>\n'
+                b'    <nd ref="8548430"/>\n'
+                b'    <nd ref="4294967296"/>\n'
+                b'    <nd ref="4294967301"/>\n'
+                b'    <nd ref="4294967298"/>\n'
+                b'    <nd ref="4294967306"/>\n'
+                b'    <nd ref="7855737"/>\n'
+                b'    <nd ref="4294967297"/>\n'
+                b'    <nd ref="4294967299"/>\n'
+                b'  </way>\n'
+                b'</modify>\n'
+                b'</osmChange>'
+            )
+        )
+
+        self.assertEquals(result[0]['type'], changesdata[0]['type'])
+        self.assertEquals(result[0]['action'], changesdata[0]['action'])
+
+        data = result[0]['data']
+        self.assertEquals(data['nd'], changesdata[0]['data']['nd'])
+        self.assertEquals(data['tag'], changesdata[0]['data']['tag'])
+        self.assertEquals(data['id'], 4294967296)
+        self.assertEquals(data['version'], 3)
+
+    def test_ChangesetUpload_delete_relation(self):
+        self._conn_mock(auth=True)
+
+        # setup mock
+        self.api.ChangesetCreate = mock.Mock(
+            return_value=4444
+        )
+        self.api._CurrentChangesetId = 4444
+
+        changesdata = [
+            {
+                'type': 'relation',
+                'action': 'delete',
+                'data': {
+                    'id': 676,
+                    'version': 2,
+                    'member': [
+                        {
+                            'ref': 4799,
+                            'role': 'outer',
+                            'type': 'way'
+                        },
+                        {
+                            'ref': 9391,
+                            'role': 'outer',
+                            'type': 'way'
+                        },
+                    ],
+                    'tag': {
+                        'admin_level': '9',
+                        'boundary': 'administrative',
+                        'type': 'multipolygon'
+                    }
+                }
+            }
+        ]
+
+        result = self.api.ChangesetUpload(changesdata)
+
+        args, _ = self.api._conn.putrequest.call_args
+        self.assertEquals(args[0], 'POST')
+        self.assertEquals(args[1], '/api/0.6/changeset/4444/upload')
+        sendargs, _ = self.api._conn.send.call_args
+        self.assertEquals(
+            xmltosorteddict(sendargs[0]),
+            xmltosorteddict(
+                b'<?xml version="1.0" encoding="UTF-8"?>\n'
+                b'<osmChange version="0.6" generator="osmapi/0.5.0">\n'
+                b'<delete>\n'
+                b'  <relation id="676" version="2" visible="true" '
+                b'changeset="4444">\n'
+                b'    <tag k="admin_level" v="9"/>\n'
+                b'    <tag k="boundary" v="administrative"/>\n'
+                b'    <tag k="type" v="multipolygon"/>\n'
+                b'    <member type="way" ref="4799" role="outer"/>\n'
+                b'    <member type="way" ref="9391" role="outer"/>\n'
+                b'  </relation>\n'
+                b'</delete>\n'
+                b'</osmChange>'
+            )
+        )
+
+        self.assertEquals(result[0]['type'], changesdata[0]['type'])
+        self.assertEquals(result[0]['action'], changesdata[0]['action'])
+
+        data = result[0]['data']
+        self.assertEquals(data['member'], changesdata[0]['data']['member'])
+        self.assertEquals(data['tag'], changesdata[0]['data']['tag'])
+        self.assertEquals(data['id'], 676)
+        self.assertNotIn('version', data)
+
+    def test_ChangesetDownload(self):
+        self._conn_mock()
+
+        result = self.api.ChangesetDownload(23123)
+
+        args, _ = self.api._conn.putrequest.call_args
+        self.assertEquals(args[0], 'GET')
+        self.assertEquals(args[1], '/api/0.6/changeset/23123/download')
+
+        self.assertEquals(len(result), 16)
+        self.assertEquals(
+            result[1],
+            {
+                'action': 'create',
+                'type': 'node',
+                'data': {
+                    'changeset': 23123,
+                    'id': 4295668171,
+                    'lat': 46.4909781,
+                    'lon': 11.2743295,
+                    'tag': {
+                        'highway': 'traffic_signals'
+                    },
+                    'timestamp': datetime.datetime(2013, 5, 14, 10, 33, 4),
+                    'uid': 1178,
+                    'user': 'tyrTester06',
+                    'version': 1,
+                    'visible': True
+                }
+            }
+        )
+
+    def test_ChangesetsGet(self):
+        self._conn_mock()
+
+        result = self.api.ChangesetsGet(
+            only_closed=True,
+            username='metaodi'
+        )
+
+        args, kwargs = self.api._conn.putrequest.call_args
+        self.assertEquals(args[0], 'GET')
+        self.assertEquals(
+            dict(urlparse.parse_qsl(urlparse.urlparse(args[1])[4])),
+            {
+                'display_name': 'metaodi',
+                'closed': '1'
+            }
+        )
+
+        self.assertEquals(len(result), 10)
+
+        self.assertEquals(result[41417], {
+            'closed_at': datetime.datetime(2014, 4, 29, 20, 25, 1),
+            'created_at': datetime.datetime(2014, 4, 29, 20, 25, 1),
+            'id': 41417,
+            'discussion': [],
+            'max_lat': '58.8997467',
+            'max_lon': '22.7364427',
+            'min_lat': '58.8501594',
+            'min_lon': '22.6984333',
+            'open': False,
+            'tag': {
+                'comment': 'Test delete of relation',
+                'created_by': 'iD 1.3.9',
+                'imagery_used': 'Bing'
+            },
+            'uid': 1841,
+            'user': 'metaodi'
+        })
+
+    def test_ChangesetGetWithComment(self):
+        self._conn_mock()
+
+        result = self.api.ChangesetGet(52924, include_discussion=True)
+
+        args, kwargs = self.api._conn.putrequest.call_args
+        self.assertEquals(args[0], 'GET')
+        self.assertEquals(
+            args[1],
+            '/api/0.6/changeset/52924?include_discussion=true'
+        )
+
+        self.assertEquals(result, {
+            'id': 52924,
+            'closed_at': datetime.datetime(2015, 1, 1, 14, 54, 2),
+            'created_at': datetime.datetime(2015, 1, 1, 14, 54, 1),
+            'comments_count': 3,
+            'max_lat': '58.3369242',
+            'max_lon': '25.8829107',
+            'min_lat': '58.336813',
+            'min_lon': '25.8823273',
+            'discussion': [
+                {
+                    'date':  datetime.datetime(2015, 1, 1, 18, 56, 48),
+                    'text': 'test',
+                    'uid': 1841,
+                    'user': 'metaodi',
+                },
+                {
+                    'date':  datetime.datetime(2015, 1, 1, 18, 58, 3),
+                    'text': 'another comment',
+                    'uid': 1841,
+                    'user': 'metaodi',
+                },
+                {
+                    'date':  datetime.datetime(2015, 1, 1, 19, 16, 5),
+                    'text': 'hello',
+                    'uid': 1841,
+                    'user': 'metaodi',
+                },
+            ],
+            'open': False,
+            'user': 'metaodi',
+            'uid': 1841,
+            'tag': {
+                'comment': 'My test',
+                'created_by': 'osmapi/0.4.1',
+            },
+        })
+
+    def test_ChangesetComment(self):
+        self._conn_mock(auth=True)
+
+        result = self.api.ChangesetComment(
+            123,
+            comment="test comment"
+        )
+
+        args, _ = self.api._conn.putrequest.call_args
+        self.assertEquals(args[0], 'POST')
+        self.assertEquals(args[1], '/api/0.6/changeset/123/comment')
+        sendargs, _ = self.api._conn.send.call_args
+        self.assertEquals(
+            sendargs[0],
+            "text=test+comment"
+        )
+        self.assertEquals(result, {
+            'id': 123,
+            'closed_at': datetime.datetime(2009, 9, 7, 22, 57, 37),
+            'created_at': datetime.datetime(2009, 9, 7, 21, 57, 36),
+            'discussion': [],
+            'max_lat': '52.4710193',
+            'max_lon': '-1.4831815',
+            'min_lat': '45.9667901',
+            'min_lon': '-1.4998534',
+            'open': False,
+            'user': 'randomjunk',
+            'uid': 3,
+            'tag': {
+                'comment': 'correct node bug',
+                'created_by': 'Potlatch 1.2a',
+            },
+        })
+
+    def test_ChangesetSubscribe(self):
+        self._conn_mock(auth=True)
+
+        result = self.api.ChangesetSubscribe(123)
+
+        args, _ = self.api._conn.putrequest.call_args
+        self.assertEquals(args[0], 'POST')
+        self.assertEquals(args[1], '/api/0.6/changeset/123/subscribe')
+        self.assertEquals(result, {
+            'id': 123,
+            'closed_at': datetime.datetime(2009, 9, 7, 22, 57, 37),
+            'created_at': datetime.datetime(2009, 9, 7, 21, 57, 36),
+            'discussion': [],
+            'max_lat': '52.4710193',
+            'max_lon': '-1.4831815',
+            'min_lat': '45.9667901',
+            'min_lon': '-1.4998534',
+            'open': False,
+            'user': 'randomjunk',
+            'uid': 3,
+            'tag': {
+                'comment': 'correct node bug',
+                'created_by': 'Potlatch 1.2a',
+            },
+        })
+
+    def test_ChangesetSubscribeWhenAlreadySubscribed(self):
+        self._conn_mock(auth=True, status=409)
+
+        with self.assertRaises(AlreadySubscribedApiError) as cm:
+            self.api.ChangesetSubscribe(52924)
+
+        self.assertEquals(cm.exception.status, 409)
+        self.assertEquals(
+            cm.exception.payload,
+            "You are already subscribed to changeset 52924."
+        )
+
+    def test_ChangesetUnsubscribe(self):
+        self._conn_mock(auth=True)
+
+        result = self.api.ChangesetUnsubscribe(123)
+
+        args, kwargs = self.api._conn.putrequest.call_args
+        self.assertEquals(args[0], 'POST')
+        self.assertEquals(args[1], '/api/0.6/changeset/123/unsubscribe')
+        self.assertEquals(result, {
+            'id': 123,
+            'closed_at': datetime.datetime(2009, 9, 7, 22, 57, 37),
+            'created_at': datetime.datetime(2009, 9, 7, 21, 57, 36),
+            'discussion': [],
+            'max_lat': '52.4710193',
+            'max_lon': '-1.4831815',
+            'min_lat': '45.9667901',
+            'min_lon': '-1.4998534',
+            'open': False,
+            'user': 'randomjunk',
+            'uid': 3,
+            'tag': {
+                'comment': 'correct node bug',
+                'created_by': 'Potlatch 1.2a',
+            },
+        })
+
+    def test_ChangesetUnsubscribeWhenNotSubscribed(self):
+        self._conn_mock(auth=True, status=404)
+
+        with self.assertRaises(NotSubscribedApiError) as cm:
+            self.api.ChangesetUnsubscribe(52924)
+
+        self.assertEquals(cm.exception.status, 404)
+        self.assertEquals(
+            cm.exception.payload,
+            "You are not subscribed to changeset 52924."
+        )
diff --git a/tests/fixtures/test_Capabilities.xml b/tests/fixtures/test_Capabilities.xml
new file mode 100644
index 0000000..e416924
--- /dev/null
+++ b/tests/fixtures/test_Capabilities.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<osm version="0.6" generator="OpenStreetMap server" copyright="OpenStreetMap and contributors" attribution="http://www.openstreetmap.org/copyright" license="http://opendatacommons.org/licenses/odbl/1-0/">
+  <api>
+    <version minimum="0.6" maximum="0.6"/>
+    <area maximum="0.25"/>
+    <tracepoints per_page="5000"/>
+    <waynodes maximum="2000"/>
+    <changesets maximum_elements="50000"/>
+    <timeout seconds="300"/>
+    <status database="online" api="mocked" gpx="online"/>
+  </api>
+</osm>
diff --git a/tests/fixtures/test_ChangesetClose.xml b/tests/fixtures/test_ChangesetClose.xml
new file mode 100644
index 0000000..e69de29
diff --git a/tests/fixtures/test_ChangesetComment.xml b/tests/fixtures/test_ChangesetComment.xml
new file mode 100644
index 0000000..8e13b53
--- /dev/null
+++ b/tests/fixtures/test_ChangesetComment.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<osm version="0.6" generator="OpenStreetMap server" copyright="OpenStreetMap and contributors" attribution="http://www.openstreetmap.org/copyright" license="http://opendatacommons.org/licenses/odbl/1-0/">
+  <changeset id="123" user="randomjunk" uid="3" created_at="2009-09-07T21:57:36Z" closed_at="2009-09-07T22:57:37Z" open="false" min_lat="45.9667901" min_lon="-1.4998534" max_lat="52.4710193" max_lon="-1.4831815">
+    <tag k="comment" v="correct node bug"/>
+    <tag k="created_by" v="Potlatch 1.2a"/>
+  </changeset>
+</osm>
diff --git a/tests/fixtures/test_ChangesetCreate.xml b/tests/fixtures/test_ChangesetCreate.xml
new file mode 100644
index 0000000..79ed404
--- /dev/null
+++ b/tests/fixtures/test_ChangesetCreate.xml
@@ -0,0 +1 @@
+4321
diff --git a/tests/fixtures/test_ChangesetCreate_with_created_by.xml b/tests/fixtures/test_ChangesetCreate_with_created_by.xml
new file mode 100644
index 0000000..81c545e
--- /dev/null
+++ b/tests/fixtures/test_ChangesetCreate_with_created_by.xml
@@ -0,0 +1 @@
+1234
diff --git a/tests/fixtures/test_ChangesetCreate_with_open_changeset.xml b/tests/fixtures/test_ChangesetCreate_with_open_changeset.xml
new file mode 100644
index 0000000..b0f6d94
--- /dev/null
+++ b/tests/fixtures/test_ChangesetCreate_with_open_changeset.xml
@@ -0,0 +1 @@
+4444
diff --git a/tests/fixtures/test_ChangesetDownload.xml b/tests/fixtures/test_ChangesetDownload.xml
new file mode 100644
index 0000000..97d0eb6
--- /dev/null
+++ b/tests/fixtures/test_ChangesetDownload.xml
@@ -0,0 +1,87 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<osmChange version="0.6" generator="OpenStreetMap server" copyright="OpenStreetMap and contributors" attribution="http://www.openstreetmap.org/copyright" license="http://opendatacommons.org/licenses/odbl/1-0/">
+  <create>
+    <node id="4295668170" changeset="23123" timestamp="2013-05-14T10:33:04Z" version="1" visible="true" user="tyrTester06" uid="1178" lat="46.4909732" lon="11.2753813">
+      <tag k="tourism" v="hotel"/>
+    </node>
+  </create>
+  <create>
+    <node id="4295668171" changeset="23123" timestamp="2013-05-14T10:33:04Z" version="1" visible="true" user="tyrTester06" uid="1178" lat="46.4909781" lon="11.2743295">
+      <tag k="highway" v="traffic_signals"/>
+    </node>
+  </create>
+  <create>
+    <node id="4295668172" changeset="23123" timestamp="2013-05-14T10:33:04Z" version="1" visible="true" user="tyrTester06" uid="1178" lat="46.4910906" lon="11.2735763">
+      <tag k="highway" v="crossing"/>
+    </node>
+  </create>
+  <create>
+    <node id="4295668173" changeset="23123" timestamp="2013-05-14T10:33:04Z" version="1" visible="true" user="tyrTester06" uid="1178" lat="46.4911004" lon="11.2759498">
+      <tag k="highway" v="bus_stop"/>
+    </node>
+  </create>
+  <create>
+    <node id="4295668174" changeset="23123" timestamp="2013-05-14T10:33:04Z" version="1" visible="true" user="tyrTester06" uid="1178" lat="46.491482" lon="11.2731001"/>
+  </create>
+  <create>
+    <node id="4295668175" changeset="23123" timestamp="2013-05-14T10:33:04Z" version="1" visible="true" user="tyrTester06" uid="1178" lat="46.4915603" lon="11.2765254"/>
+  </create>
+  <create>
+    <node id="4295668176" changeset="23123" timestamp="2013-05-14T10:33:04Z" version="1" visible="true" user="tyrTester06" uid="1178" lat="46.4919468" lon="11.2756726"/>
+  </create>
+  <create>
+    <node id="4295668177" changeset="23123" timestamp="2013-05-14T10:33:04Z" version="1" visible="true" user="tyrTester06" uid="1178" lat="46.4919664" lon="11.2753031"/>
+  </create>
+  <create>
+    <node id="4295668178" changeset="23123" timestamp="2013-05-14T10:33:04Z" version="1" visible="true" user="tyrTester06" uid="1178" lat="46.4921083" lon="11.2755021"/>
+  </create>
+  <create>
+    <node id="4295668179" changeset="23123" timestamp="2013-05-14T10:33:04Z" version="1" visible="true" user="tyrTester06" uid="1178" lat="46.4921327" lon="11.2742229"/>
+  </create>
+  <create>
+    <node id="4295668180" changeset="23123" timestamp="2013-05-14T10:33:04Z" version="1" visible="true" user="tyrTester06" uid="1178" lat="46.4922893" lon="11.2757152"/>
+  </create>
+  <create>
+    <node id="4295668181" changeset="23123" timestamp="2013-05-14T10:33:04Z" version="1" visible="true" user="tyrTester06" uid="1178" lat="46.4923235" lon="11.2752747"/>
+  </create>
+  <create>
+    <way id="4295032193" changeset="23123" timestamp="2013-05-14T10:33:04Z" version="1" visible="true" user="tyrTester06" uid="1178">
+      <nd ref="4295668181"/>
+      <nd ref="4295668178"/>
+      <nd ref="4295668176"/>
+      <tag k="highway" v="service"/>
+    </way>
+  </create>
+  <create>
+    <way id="4295032194" changeset="23123" timestamp="2013-05-14T10:33:05Z" version="1" visible="true" user="tyrTester06" uid="1178">
+      <nd ref="4295668177"/>
+      <nd ref="4295668178"/>
+      <nd ref="4295668180"/>
+      <tag k="highway" v="service"/>
+    </way>
+  </create>
+  <create>
+    <way id="4295032195" changeset="23123" timestamp="2013-05-14T10:33:05Z" version="1" visible="true" user="tyrTester06" uid="1178">
+      <nd ref="4295668174"/>
+      <nd ref="4295668172"/>
+      <nd ref="4295668171"/>
+      <nd ref="4295668170"/>
+      <nd ref="4295668173"/>
+      <nd ref="4295668175"/>
+      <tag k="highway" v="residential"/>
+    </way>
+  </create>
+  <create>
+    <relation id="4294968148" changeset="23123" timestamp="2013-05-14T10:33:05Z" version="1" visible="true" user="tyrTester06" uid="1178">
+      <member type="way" ref="4295032195" role="line"/>
+      <member type="node" ref="4295668179" role="point"/>
+      <member type="node" ref="4295668178" role=""/>
+      <member type="way" ref="4295032194" role=""/>
+      <member type="way" ref="4295032193" role=""/>
+      <member type="node" ref="4295668174" role="foo"/>
+      <member type="node" ref="4295668175" role="bar"/>
+      <tag k="type" v="fancy"/>
+    </relation>
+  </create>
+</osmChange>
+
diff --git a/tests/fixtures/test_ChangesetGet.xml b/tests/fixtures/test_ChangesetGet.xml
new file mode 100644
index 0000000..8e13b53
--- /dev/null
+++ b/tests/fixtures/test_ChangesetGet.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<osm version="0.6" generator="OpenStreetMap server" copyright="OpenStreetMap and contributors" attribution="http://www.openstreetmap.org/copyright" license="http://opendatacommons.org/licenses/odbl/1-0/">
+  <changeset id="123" user="randomjunk" uid="3" created_at="2009-09-07T21:57:36Z" closed_at="2009-09-07T22:57:37Z" open="false" min_lat="45.9667901" min_lon="-1.4998534" max_lat="52.4710193" max_lon="-1.4831815">
+    <tag k="comment" v="correct node bug"/>
+    <tag k="created_by" v="Potlatch 1.2a"/>
+  </changeset>
+</osm>
diff --git a/tests/fixtures/test_ChangesetGetWithComment.xml b/tests/fixtures/test_ChangesetGetWithComment.xml
new file mode 100644
index 0000000..2702340
--- /dev/null
+++ b/tests/fixtures/test_ChangesetGetWithComment.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<osm version="0.6" generator="OpenStreetMap server" copyright="OpenStreetMap and contributors" attribution="http://www.openstreetmap.org/copyright" license="http://opendatacommons.org/licenses/odbl/1-0/">
+  <changeset id="52924" user="metaodi" uid="1841" created_at="2015-01-01T14:54:01Z" closed_at="2015-01-01T14:54:02Z" open="false" min_lat="58.336813" min_lon="25.8823273" max_lat="58.3369242" max_lon="25.8829107" comments_count="3">
+    <tag k="created_by" v="osmapi/0.4.1"/>
+    <tag k="comment" v="My test"/>
+    <discussion>
+      <comment date="2015-01-01T18:56:48Z" uid="1841" user="metaodi">
+        <text>test</text>
+      </comment>
+      <comment date="2015-01-01T18:58:03Z" uid="1841" user="metaodi">
+        <text>another comment</text>
+      </comment>
+      <comment date="2015-01-01T19:16:05Z" uid="1841" user="metaodi">
+        <text>hello</text>
+      </comment>
+    </discussion>
+  </changeset>
+</osm>
diff --git a/tests/fixtures/test_ChangesetSubscribe.xml b/tests/fixtures/test_ChangesetSubscribe.xml
new file mode 100644
index 0000000..8e13b53
--- /dev/null
+++ b/tests/fixtures/test_ChangesetSubscribe.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<osm version="0.6" generator="OpenStreetMap server" copyright="OpenStreetMap and contributors" attribution="http://www.openstreetmap.org/copyright" license="http://opendatacommons.org/licenses/odbl/1-0/">
+  <changeset id="123" user="randomjunk" uid="3" created_at="2009-09-07T21:57:36Z" closed_at="2009-09-07T22:57:37Z" open="false" min_lat="45.9667901" min_lon="-1.4998534" max_lat="52.4710193" max_lon="-1.4831815">
+    <tag k="comment" v="correct node bug"/>
+    <tag k="created_by" v="Potlatch 1.2a"/>
+  </changeset>
+</osm>
diff --git a/tests/fixtures/test_ChangesetSubscribeWhenAlreadySubscribed.xml b/tests/fixtures/test_ChangesetSubscribeWhenAlreadySubscribed.xml
new file mode 100644
index 0000000..11e71cb
--- /dev/null
+++ b/tests/fixtures/test_ChangesetSubscribeWhenAlreadySubscribed.xml
@@ -0,0 +1 @@
+You are already subscribed to changeset 52924.
diff --git a/tests/fixtures/test_ChangesetUnsubscribe.xml b/tests/fixtures/test_ChangesetUnsubscribe.xml
new file mode 100644
index 0000000..8e13b53
--- /dev/null
+++ b/tests/fixtures/test_ChangesetUnsubscribe.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<osm version="0.6" generator="OpenStreetMap server" copyright="OpenStreetMap and contributors" attribution="http://www.openstreetmap.org/copyright" license="http://opendatacommons.org/licenses/odbl/1-0/">
+  <changeset id="123" user="randomjunk" uid="3" created_at="2009-09-07T21:57:36Z" closed_at="2009-09-07T22:57:37Z" open="false" min_lat="45.9667901" min_lon="-1.4998534" max_lat="52.4710193" max_lon="-1.4831815">
+    <tag k="comment" v="correct node bug"/>
+    <tag k="created_by" v="Potlatch 1.2a"/>
+  </changeset>
+</osm>
diff --git a/tests/fixtures/test_ChangesetUnsubscribeWhenNotSubscribed.xml b/tests/fixtures/test_ChangesetUnsubscribeWhenNotSubscribed.xml
new file mode 100644
index 0000000..69d1685
--- /dev/null
+++ b/tests/fixtures/test_ChangesetUnsubscribeWhenNotSubscribed.xml
@@ -0,0 +1 @@
+You are not subscribed to changeset 52924.
diff --git a/tests/fixtures/test_ChangesetUpdate.xml b/tests/fixtures/test_ChangesetUpdate.xml
new file mode 100644
index 0000000..b0f6d94
--- /dev/null
+++ b/tests/fixtures/test_ChangesetUpdate.xml
@@ -0,0 +1 @@
+4444
diff --git a/tests/fixtures/test_ChangesetUpdate_with_created_by.xml b/tests/fixtures/test_ChangesetUpdate_with_created_by.xml
new file mode 100644
index 0000000..b0f6d94
--- /dev/null
+++ b/tests/fixtures/test_ChangesetUpdate_with_created_by.xml
@@ -0,0 +1 @@
+4444
diff --git a/tests/fixtures/test_ChangesetUpdate_wo_changeset.xml b/tests/fixtures/test_ChangesetUpdate_wo_changeset.xml
new file mode 100644
index 0000000..e69de29
diff --git a/tests/fixtures/test_ChangesetUpload_create_node.xml b/tests/fixtures/test_ChangesetUpload_create_node.xml
new file mode 100644
index 0000000..400a800
--- /dev/null
+++ b/tests/fixtures/test_ChangesetUpload_create_node.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<diffResult version="0.6" generator="OpenStreetMap server" copyright="OpenStreetMap and contributors" attribution="http://www.openstreetmap.org/copyright" license="http://opendatacommons.org/licenses/odbl/1-0/">
+    <node old_id="0" new_id="4295832900" new_version="1"/>
+</diffResult>
+
diff --git a/tests/fixtures/test_ChangesetUpload_delete_relation.xml b/tests/fixtures/test_ChangesetUpload_delete_relation.xml
new file mode 100644
index 0000000..c255189
--- /dev/null
+++ b/tests/fixtures/test_ChangesetUpload_delete_relation.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<diffResult version="0.6" generator="OpenStreetMap server" copyright="OpenStreetMap and contributors" attribution="http://www.openstreetmap.org/copyright" license="http://opendatacommons.org/licenses/odbl/1-0/">
+    <relation old_id="676"/>
+</diffResult>
diff --git a/tests/fixtures/test_ChangesetUpload_modify_way.xml b/tests/fixtures/test_ChangesetUpload_modify_way.xml
new file mode 100644
index 0000000..ac0e423
--- /dev/null
+++ b/tests/fixtures/test_ChangesetUpload_modify_way.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<diffResult version="0.6" generator="OpenStreetMap server" copyright="OpenStreetMap and contributors" attribution="http://www.openstreetmap.org/copyright" license="http://opendatacommons.org/licenses/odbl/1-0/">
+    <way old_id="4294967296" new_id="4294967296" new_version="3"/>
+</diffResult>
diff --git a/tests/fixtures/test_ChangesetsGet.xml b/tests/fixtures/test_ChangesetsGet.xml
new file mode 100644
index 0000000..e3c7cf7
--- /dev/null
+++ b/tests/fixtures/test_ChangesetsGet.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<osm version="0.6" generator="OpenStreetMap server" copyright="OpenStreetMap and contributors" attribution="http://www.openstreetmap.org/copyright" license="http://opendatacommons.org/licenses/odbl/1-0/">
+  <changeset id="41418" user="metaodi" uid="1841" created_at="2014-04-29T20:28:55Z" closed_at="2014-04-29T21:28:55Z" open="false">
+    <tag k="created_by" v="osmapi/0.2.24"/>
+  </changeset>
+  <changeset id="41417" user="metaodi" uid="1841" created_at="2014-04-29T20:25:01Z" closed_at="2014-04-29T20:25:01Z" open="false" min_lat="58.8501594" min_lon="22.6984333" max_lat="58.8997467" max_lon="22.7364427">
+    <tag k="comment" v="Test delete of relation"/>
+    <tag k="created_by" v="iD 1.3.9"/>
+    <tag k="imagery_used" v="Bing"/>
+  </changeset>
+  <changeset id="41416" user="metaodi" uid="1841" created_at="2014-04-29T20:04:15Z" closed_at="2014-04-29T21:04:15Z" open="false">
+    <tag k="created_by" v="osmapi/0.2.24"/>
+  </changeset>
+  <changeset id="41415" user="metaodi" uid="1841" created_at="2014-04-29T20:02:33Z" closed_at="2014-04-29T21:02:33Z" open="false" min_lat="51.7679308" min_lon="-0.007848" max_lat="51.7746244" max_lon="-0.0030047">
+    <tag k="created_by" v="osmapi/0.2.24"/>
+  </changeset>
+  <changeset id="41414" user="metaodi" uid="1841" created_at="2014-04-29T19:41:29Z" closed_at="2014-04-29T20:41:29Z" open="false" min_lat="47.123" min_lon="8.555" max_lat="47.123" max_lon="8.555">
+    <tag k="created_by" v="osmapi/0.2.24"/>
+  </changeset>
+  <changeset id="41413" user="metaodi" uid="1841" created_at="2014-04-29T19:40:55Z" closed_at="2014-04-29T20:40:55Z" open="false">
+    <tag k="created_by" v="osmapi/0.2.24"/>
+  </changeset>
+  <changeset id="41412" user="metaodi" uid="1841" created_at="2014-04-29T19:39:01Z" closed_at="2014-04-29T20:39:01Z" open="false">
+    <tag k="created_by" v="osmapi/0.2.24"/>
+  </changeset>
+  <changeset id="41411" user="metaodi" uid="1841" created_at="2014-04-29T19:38:36Z" closed_at="2014-04-29T20:38:36Z" open="false">
+    <tag k="created_by" v="osmapi/0.2.24"/>
+  </changeset>
+  <changeset id="41378" user="metaodi" uid="1841" created_at="2014-04-26T08:19:22Z" closed_at="2014-04-26T08:19:23Z" open="false" min_lat="57.7554949" min_lon="27.0236872" max_lat="57.7819087" max_lon="27.0617344">
+    <tag k="comment" v="Continue line to roundabout"/>
+    <tag k="created_by" v="iD 1.3.9"/>
+    <tag k="imagery_used" v="Bing"/>
+  </changeset>
+  <changeset id="41303" user="metaodi" uid="1841" created_at="2014-04-23T15:02:03Z" closed_at="2014-04-23T15:02:05Z" open="false" min_lat="51.7679308" min_lon="-0.007848" max_lat="51.7746244" max_lon="-0.0030047">
+    <tag k="comment" v="Continue line to roundabout"/>
+    <tag k="created_by" v="iD 1.3.9"/>
+    <tag k="imagery_used" v="Bing"/>
+  </changeset>
+</osm>
diff --git a/tests/fixtures/test_NodeCreate.xml b/tests/fixtures/test_NodeCreate.xml
new file mode 100644
index 0000000..c17f72f
--- /dev/null
+++ b/tests/fixtures/test_NodeCreate.xml
@@ -0,0 +1 @@
+9876
diff --git a/tests/fixtures/test_NodeCreate_changesetauto.xml b/tests/fixtures/test_NodeCreate_changesetauto.xml
new file mode 100644
index 0000000..bcc05ae
--- /dev/null
+++ b/tests/fixtures/test_NodeCreate_changesetauto.xml
@@ -0,0 +1 @@
+7676
diff --git a/tests/fixtures/test_NodeDelete.xml b/tests/fixtures/test_NodeDelete.xml
new file mode 100644
index 0000000..b8626c4
--- /dev/null
+++ b/tests/fixtures/test_NodeDelete.xml
@@ -0,0 +1 @@
+4
diff --git a/tests/fixtures/test_NodeGet.xml b/tests/fixtures/test_NodeGet.xml
new file mode 100644
index 0000000..ea86bc3
--- /dev/null
+++ b/tests/fixtures/test_NodeGet.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<osm version="0.6" generator="OpenStreetMap server" copyright="OpenStreetMap and contributors" attribution="http://www.openstreetmap.org/copyright" license="http://opendatacommons.org/licenses/odbl/1-0/">
+  <node id="123" changeset="15293" timestamp="2012-04-18T11:14:26Z" version="8" visible="true" user="freundchen" uid="605" lat="51.8753146" lon="-1.4857118">
+    <tag k="amenity" v="school"/>
+    <tag k="foo" v="bar"/>
+    <tag k="name" v="Berolina & Schule"/>
+  </node>
+</osm>
diff --git a/tests/fixtures/test_NodeGet_with_version.xml b/tests/fixtures/test_NodeGet_with_version.xml
new file mode 100644
index 0000000..f0bb689
--- /dev/null
+++ b/tests/fixtures/test_NodeGet_with_version.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<osm version="0.6" generator="OpenStreetMap server" copyright="OpenStreetMap and contributors" attribution="http://www.openstreetmap.org/copyright" license="http://opendatacommons.org/licenses/odbl/1-0/">
+  <node id="123" changeset="4152" timestamp="2011-04-18T11:14:26Z" version="2" visible="true" user="freundchen" uid="605" lat="51.8753146" lon="-1.4857118">
+    <tag k="amenity" v="school"/>
+  </node>
+</osm>
diff --git a/tests/fixtures/test_NodeHistory.xml b/tests/fixtures/test_NodeHistory.xml
new file mode 100644
index 0000000..b4a0941
--- /dev/null
+++ b/tests/fixtures/test_NodeHistory.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<osm version="0.6" generator="OpenStreetMap server" copyright="OpenStreetMap and contributors" attribution="http://www.openstreetmap.org/copyright" license="http://opendatacommons.org/licenses/odbl/1-0/">
+  <node id="123" changeset="25" timestamp="2009-09-04T14:59:31Z" version="1" visible="true" user="randomjunk" uid="3" lat="51.8750241" lon="-1.4852244"/>
+  <node id="123" changeset="26" timestamp="2009-09-04T15:20:41Z" version="2" visible="true" user="randomjunk" uid="3" lat="51.8750241" lon="-1.4852244"/>
+  <node id="123" changeset="7283" timestamp="2010-09-01T18:31:11Z" version="3" visible="true" user="gravitystorm" uid="17" lat="51.8753146" lon="-1.4857118">
+    <tag k="foo" v="bar"/>
+  </node>
+  <node id="123" changeset="13688" timestamp="2012-02-16T10:45:01Z" version="4" visible="true" user="schmerzbereiter" uid="152" lat="51.8753146" lon="-1.4857118">
+    <tag k="empty" v=""/>
+    <tag k="foo" v="bar"/>
+  </node>
+  <node id="123" changeset="13688" timestamp="2012-02-16T10:47:38Z" version="5" visible="true" user="schmerzbereiter" uid="152" lat="51.8753146" lon="-1.4857118">
+    <tag k="foo" v="bar"/>
+  </node>
+  <node id="123" changeset="15219" timestamp="2012-04-13T12:16:20Z" version="6" visible="true" user="freundchen" uid="605" lat="51.8753146" lon="-1.4857118">
+    <tag k="foo" v="bar"/>
+    <tag k="name" v="murx"/>
+  </node>
+  <node id="123" changeset="15220" timestamp="2012-04-13T12:39:08Z" version="7" visible="true" user="freundchen" uid="605" lat="51.8753146" lon="-1.4857118">
+    <tag k="foo" v="bar"/>
+    <tag k="name" v="blblbbl"/>
+  </node>
+  <node id="123" changeset="15293" timestamp="2012-04-18T11:14:26Z" version="8" visible="true" user="freundchen" uid="605" lat="51.8753146" lon="-1.4857118">
+    <tag k="amenity" v="school"/>
+    <tag k="foo" v="bar"/>
+    <tag k="name" v="Berolina & Schule"/>
+    <tag k="wheelchair" v="unknown"/>
+  </node>
+</osm>
diff --git a/tests/fixtures/test_NodeRelations.xml b/tests/fixtures/test_NodeRelations.xml
new file mode 100644
index 0000000..aba6a8f
--- /dev/null
+++ b/tests/fixtures/test_NodeRelations.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<osm version="0.6" generator="OpenStreetMap server" copyright="OpenStreetMap and contributors" attribution="http://www.openstreetmap.org/copyright" license="http://opendatacommons.org/licenses/odbl/1-0/">
+  <relation id="4294968148" changeset="23123" timestamp="2013-05-14T10:33:05Z" version="1" visible="true" user="tyrTester06" uid="1178">
+    <member type="way" ref="4295032195" role="line"/>
+    <member type="node" ref="4295668179" role="point"/>
+    <member type="node" ref="4295668178" role=""/>
+    <member type="way" ref="4295032194" role=""/>
+    <member type="way" ref="4295032193" role=""/>
+    <member type="node" ref="4295668174" role="foo"/>
+    <member type="node" ref="4295668175" role="bar"/>
+    <tag k="type" v="fancy"/>
+  </relation>
+</osm>
diff --git a/tests/fixtures/test_NodeUpdate.xml b/tests/fixtures/test_NodeUpdate.xml
new file mode 100644
index 0000000..00750ed
--- /dev/null
+++ b/tests/fixtures/test_NodeUpdate.xml
@@ -0,0 +1 @@
+3
diff --git a/tests/fixtures/test_NodeWays.xml b/tests/fixtures/test_NodeWays.xml
new file mode 100644
index 0000000..c1d26d1
--- /dev/null
+++ b/tests/fixtures/test_NodeWays.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<osm version="0.6" generator="OpenStreetMap server" copyright="OpenStreetMap and contributors" attribution="http://www.openstreetmap.org/copyright" license="http://opendatacommons.org/licenses/odbl/1-0/">
+  <way id="60" changeset="61" timestamp="2009-09-05T21:30:21Z" version="1" visible="true" user="costello" uid="8">
+    <nd ref="232"/>
+    <nd ref="233"/>
+    <nd ref="234"/>
+    <nd ref="235"/>
+    <nd ref="236"/>
+    <nd ref="237"/>
+    <tag k="highway" v="path"/>
+    <tag k="name" v="Dog walking path"/>
+  </way>
+</osm>
diff --git a/tests/fixtures/test_NodesGet.xml b/tests/fixtures/test_NodesGet.xml
new file mode 100644
index 0000000..6f715f8
--- /dev/null
+++ b/tests/fixtures/test_NodesGet.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<osm version="0.6" generator="OpenStreetMap server" copyright="OpenStreetMap and contributors" attribution="http://www.openstreetmap.org/copyright" license="http://opendatacommons.org/licenses/odbl/1-0/">
+  <node id="345" changeset="244" timestamp="2009-09-12T03:22:59Z" version="2" visible="false" user="guggis" uid="1"/>
+  <node id="123" changeset="15293" timestamp="2012-04-18T11:14:26Z" version="8" visible="true" user="freundchen" uid="605" lat="51.8753146" lon="-1.4857118">
+    <tag k="amenity" v="school"/>
+    <tag k="foo" v="bar"/>
+    <tag k="name" v="Berolina & Schule"/>
+  </node>
+</osm>
diff --git a/tests/fixtures/test_NoteClose.xml b/tests/fixtures/test_NoteClose.xml
new file mode 100644
index 0000000..63f75a3
--- /dev/null
+++ b/tests/fixtures/test_NoteClose.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<osm version="0.6" generator="OpenStreetMap server">
+<note lon="8.432" lat="47.123">
+  <id>815</id>
+  <url>http://api06.dev.openstreetmap.org/api/0.6/notes/815</url>
+  <reopen_url>http://api06.dev.openstreetmap.org/api/0.6/notes/815/reopen</reopen_url>
+  <date_created>2014-10-03 15:20:57 UTC</date_created>
+  <status>closed</status>
+  <date_closed>2014-10-05 16:35:13 UTC</date_closed>
+  <comments>
+    <comment>
+      <date>2014-10-03 15:20:57 UTC</date>
+      <uid>1841</uid>
+      <user>metaodi</user>
+      <user_url>http://master.apis.dev.openstreetmap.org/user/metaodi</user_url>
+      <action>opened</action>
+      <text>This is a test</text>
+      <html><p>This is a test</p></html>
+    </comment>
+    <comment>
+      <date>2014-10-05 16:35:13 UTC</date>
+      <uid>1841</uid>
+      <user>metaodi</user>
+      <user_url>http://master.apis.dev.openstreetmap.org/user/metaodi</user_url>
+      <action>closed</action>
+      <text>Close this note!</text>
+      <html><p>Close this note!</p></html>
+    </comment>
+  </comments>
+</note>
+</osm>
diff --git a/tests/fixtures/test_NoteComment.xml b/tests/fixtures/test_NoteComment.xml
new file mode 100644
index 0000000..f7614dd
--- /dev/null
+++ b/tests/fixtures/test_NoteComment.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<osm version="0.6" generator="OpenStreetMap server">
+<note lon="8.432" lat="47.123">
+  <id>812</id>
+  <url>http://api06.dev.openstreetmap.org/api/0.6/notes/812</url>
+  <comment_url>http://api06.dev.openstreetmap.org/api/0.6/notes/812/comment</comment_url>
+  <close_url>http://api06.dev.openstreetmap.org/api/0.6/notes/812/close</close_url>
+  <date_created>2014-10-03 15:11:05 UTC</date_created>
+  <status>open</status>
+  <comments>
+    <comment>
+      <date>2014-10-03 15:11:05 UTC</date>
+      <uid>1841</uid>
+      <user>metaodi</user>
+      <user_url>http://master.apis.dev.openstreetmap.org/user/metaodi</user_url>
+      <action>opened</action>
+      <text>This is a test</text>
+      <html><p>This is a test</p></html>
+    </comment>
+    <comment>
+      <date>2014-10-04 22:36:35 UTC</date>
+      <uid>1841</uid>
+      <user>metaodi</user>
+      <user_url>http://master.apis.dev.openstreetmap.org/user/metaodi</user_url>
+      <action>commented</action>
+      <text>This is a comment</text>
+      <html><p>This is a comment</p></html>
+    </comment>
+  </comments>
+</note>
+</osm>
diff --git a/tests/fixtures/test_NoteCommentAnonymous.xml b/tests/fixtures/test_NoteCommentAnonymous.xml
new file mode 100644
index 0000000..772934e
--- /dev/null
+++ b/tests/fixtures/test_NoteCommentAnonymous.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<osm version="0.6" generator="OpenStreetMap server">
+<note lon="25.8826183" lat="58.3368222">
+  <id>842</id>
+  <url>http://api06.dev.openstreetmap.org/api/0.6/notes/842</url>
+  <comment_url>http://api06.dev.openstreetmap.org/api/0.6/notes/842/comment</comment_url>
+  <close_url>http://api06.dev.openstreetmap.org/api/0.6/notes/842/close</close_url>
+  <date_created>2015-01-03 10:49:39 UTC</date_created>
+  <status>open</status>
+  <comments>
+    <comment>
+      <date>2015-01-03 10:49:39 UTC</date>
+      <action>opened</action>
+      <text>test 123</text>
+      <html><p>test 123</p></html>
+    </comment>
+    <comment>
+      <date>2015-01-03 11:06:00 UTC</date>
+      <action>commented</action>
+      <text>blubb</text>
+      <html><p>blubb</p></html>
+    </comment>
+  </comments>
+</note>
+</osm>
diff --git a/tests/fixtures/test_NoteCreate.xml b/tests/fixtures/test_NoteCreate.xml
new file mode 100644
index 0000000..b0944a0
--- /dev/null
+++ b/tests/fixtures/test_NoteCreate.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<osm version="0.6" generator="OpenStreetMap server">
+<note lon="8.432" lat="47.123">
+  <id>816</id>
+  <url>http://api06.dev.openstreetmap.org/api/0.6/notes/816</url>
+  <comment_url>http://api06.dev.openstreetmap.org/api/0.6/notes/816/comment</comment_url>
+  <close_url>http://api06.dev.openstreetmap.org/api/0.6/notes/816/close</close_url>
+  <date_created>2014-10-03 15:21:21 UTC</date_created>
+  <status>open</status>
+  <comments>
+    <comment>
+      <date>2014-10-03 15:21:22 UTC</date>
+      <uid>1841</uid>
+      <user>metaodi</user>
+      <user_url>http://master.apis.dev.openstreetmap.org/user/metaodi</user_url>
+      <action>opened</action>
+      <text>This is a test</text>
+      <html><p>This is a test</p></html>
+    </comment>
+  </comments>
+</note>
+</osm>
diff --git a/tests/fixtures/test_NoteCreateAnonymous.xml b/tests/fixtures/test_NoteCreateAnonymous.xml
new file mode 100644
index 0000000..5dece43
--- /dev/null
+++ b/tests/fixtures/test_NoteCreateAnonymous.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<osm version="0.6" generator="OpenStreetMap server">
+<note lon="25.8826183" lat="58.3368222">
+  <id>842</id>
+  <url>http://api06.dev.openstreetmap.org/api/0.6/notes/842</url>
+  <comment_url>http://api06.dev.openstreetmap.org/api/0.6/notes/842/comment</comment_url>
+  <close_url>http://api06.dev.openstreetmap.org/api/0.6/notes/842/close</close_url>
+  <date_created>2015-01-03 10:49:39 UTC</date_created>
+  <status>open</status>
+  <comments>
+    <comment>
+      <date>2015-01-03 10:49:39 UTC</date>
+      <action>opened</action>
+      <text>test 123</text>
+      <html><p>test 123</p></html>
+    </comment>
+  </comments>
+</note>
+</osm>
diff --git a/tests/fixtures/test_NoteCreate_wo_auth.xml b/tests/fixtures/test_NoteCreate_wo_auth.xml
new file mode 100644
index 0000000..b23f36c
--- /dev/null
+++ b/tests/fixtures/test_NoteCreate_wo_auth.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<osm version="0.6" generator="OpenStreetMap server">
+<note lon="8.432" lat="47.123">
+  <id>824</id>
+  <url>http://api06.dev.openstreetmap.org/api/0.6/notes/824</url>
+  <comment_url>http://api06.dev.openstreetmap.org/api/0.6/notes/824/comment</comment_url>
+  <close_url>http://api06.dev.openstreetmap.org/api/0.6/notes/824/close</close_url>
+  <date_created>2014-10-03 16:09:11 UTC</date_created>
+  <status>open</status>
+  <comments>
+    <comment>
+      <date>2014-10-03 16:09:12 UTC</date>
+      <action>opened</action>
+      <text>This is an unauthenticated test</text>
+      <html><p>This is an unauthenticated test</p></html>
+    </comment>
+  </comments>
+</note>
+</osm>
+
+{u'comments': [{u'action': u'opened',
+                u'date': u'2014-10-03 16:09:12 UTC',
+                u'html': u'<p>This is an unauthenticated test</p>',
+                u'text': u'This is an unauthenticated test',
+                u'uid': None,
+                u'user': None}],
+ u'date_closed': None,
+ u'date_created': u'2014-10-03 16:09:11 UTC',
+ u'id': u'824',
+ u'lat': 47.123,
+ u'lon': 8.432,
+ u'status': u'open'}
diff --git a/tests/fixtures/test_NoteGet.xml b/tests/fixtures/test_NoteGet.xml
new file mode 100644
index 0000000..a35b173
--- /dev/null
+++ b/tests/fixtures/test_NoteGet.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<osm version="0.6" generator="OpenStreetMap server">
+<note lon="12.3133135" lat="37.9305489">
+  <id>1111</id>
+  <url>http://api.openstreetmap.org/api/0.6/notes/1111</url>
+  <reopen_url>http://api.openstreetmap.org/api/0.6/notes/1111/reopen</reopen_url>
+  <date_created>2013-05-01 20:58:21 UTC</date_created>
+  <status>closed</status>
+  <date_closed>2013-08-21 16:43:26 UTC</date_closed>
+  <comments>
+    <comment>
+      <date>2013-05-01 20:58:21 UTC</date>
+      <uid>1363438</uid>
+      <user>giuseppemari</user>
+      <user_url>http://www.openstreetmap.org/user/giuseppemari</user_url>
+      <action>opened</action>
+      <text>It does not exist this path</text>
+      <html><p>It does not exist this path</p></html>
+    </comment>
+    <comment>
+      <date>2013-08-21 16:43:26 UTC</date>
+      <uid>1714220</uid>
+      <user>luschi</user>
+      <user_url>http://www.openstreetmap.org/user/luschi</user_url>
+      <action>closed</action>
+      <text>there is no path signed</text>
+      <html><p>there is no path signed</p></html>
+    </comment>
+  </comments>
+</note>
+</osm>
diff --git a/tests/fixtures/test_NoteReopen.xml b/tests/fixtures/test_NoteReopen.xml
new file mode 100644
index 0000000..88a257b
--- /dev/null
+++ b/tests/fixtures/test_NoteReopen.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<osm version="0.6" generator="OpenStreetMap server">
+<note lon="8.432" lat="47.123">
+  <id>815</id>
+  <url>http://api06.dev.openstreetmap.org/api/0.6/notes/815</url>
+  <comment_url>http://api06.dev.openstreetmap.org/api/0.6/notes/815/comment</comment_url>
+  <close_url>http://api06.dev.openstreetmap.org/api/0.6/notes/815/close</close_url>
+  <date_created>2014-10-03 15:20:57 UTC</date_created>
+  <status>open</status>
+  <comments>
+    <comment>
+      <date>2014-10-03 15:20:57 UTC</date>
+      <uid>1841</uid>
+      <user>metaodi</user>
+      <user_url>http://master.apis.dev.openstreetmap.org/user/metaodi</user_url>
+      <action>opened</action>
+      <text>This is a test</text>
+      <html><p>This is a test</p></html>
+    </comment>
+    <comment>
+      <date>2014-10-05 16:35:13 UTC</date>
+      <uid>1841</uid>
+      <user>metaodi</user>
+      <user_url>http://master.apis.dev.openstreetmap.org/user/metaodi</user_url>
+      <action>closed</action>
+      <text>Close this note!</text>
+      <html><p>Close this note!</p></html>
+    </comment>
+    <comment>
+      <date>2014-10-05 16:44:56 UTC</date>
+      <uid>1841</uid>
+      <user>metaodi</user>
+      <user_url>http://master.apis.dev.openstreetmap.org/user/metaodi</user_url>
+      <action>reopened</action>
+      <text>Reopen this note!</text>
+      <html><p>Reopen this note!</p></html>
+    </comment>
+  </comments>
+</note>
+</osm>
diff --git a/tests/fixtures/test_NotesGet.xml b/tests/fixtures/test_NotesGet.xml
new file mode 100644
index 0000000..b8cb5a4
--- /dev/null
+++ b/tests/fixtures/test_NotesGet.xml
@@ -0,0 +1,441 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<osm version="0.6" generator="OpenStreetMap server">
+<note lon="-1.4844471" lat="52.4190649">
+  <id>231776</id>
+  <url>http://www.openstreetmap.org/api/0.6/notes/231776</url>
+  <reopen_url>http://www.openstreetmap.org/api/0.6/notes/231776/reopen</reopen_url>
+  <date_created>2014-08-28 19:27:13 UTC</date_created>
+  <status>closed</status>
+  <date_closed>2014-09-27 09:27:48 UTC</date_closed>
+  <comments>
+    <comment>
+      <date>2014-08-28 19:27:13 UTC</date>
+      <uid>1486336</uid>
+      <user>Wyken Seagrave</user>
+      <user_url>http://www.openstreetmap.org/user/Wyken%20Seagrave</user_url>
+      <action>opened</action>
+      <text>Is it Pinners or Pinner's ?</text>
+      <html><p>Is it Pinners or Pinner's ?</p></html>
+    </comment>
+    <comment>
+      <date>2014-09-26 13:10:36 UTC</date>
+      <action>commented</action>
+      <text>Royal Mail's postcode finder has PINNERS CROFT.</text>
+      <html><p>Royal Mail's postcode finder has PINNERS CROFT.</p></html>
+    </comment>
+    <comment>
+      <date>2014-09-27 09:27:48 UTC</date>
+      <uid>1486336</uid>
+      <user>Wyken Seagrave</user>
+      <user_url>http://www.openstreetmap.org/user/Wyken%20Seagrave</user_url>
+      <action>closed</action>
+      <text>Pinners without apostrophe is correct, based on survey</text>
+      <html><p>Pinners without apostrophe is correct, based on survey</p></html>
+    </comment>
+  </comments>
+</note>
+<note lon="-1.4950687" lat="52.420951">
+  <id>231733</id>
+  <url>http://www.openstreetmap.org/api/0.6/notes/231733</url>
+  <reopen_url>http://www.openstreetmap.org/api/0.6/notes/231733/reopen</reopen_url>
+  <date_created>2014-08-28 18:39:05 UTC</date_created>
+  <status>closed</status>
+  <date_closed>2014-09-27 09:24:55 UTC</date_closed>
+  <comments>
+    <comment>
+      <date>2014-08-28 18:39:05 UTC</date>
+      <uid>1486336</uid>
+      <user>Wyken Seagrave</user>
+      <user_url>http://www.openstreetmap.org/user/Wyken%20Seagrave</user_url>
+      <action>opened</action>
+      <text>Should be Finbarr? </text>
+      <html><p>Should be Finbarr? </p></html>
+    </comment>
+    <comment>
+      <date>2014-09-26 13:09:14 UTC</date>
+      <action>commented</action>
+      <text>You're right. Royal Mail's postcode finder has FINBARR CLOSE.</text>
+      <html><p>You're right. Royal Mail's postcode finder has FINBARR CLOSE.</p></html>
+    </comment>
+    <comment>
+      <date>2014-09-27 09:24:55 UTC</date>
+      <uid>1486336</uid>
+      <user>Wyken Seagrave</user>
+      <user_url>http://www.openstreetmap.org/user/Wyken%20Seagrave</user_url>
+      <action>closed</action>
+      <text>Finbar is name on street plate, verified by survey</text>
+      <html><p>Finbar is name on street plate, verified by survey</p></html>
+    </comment>
+  </comments>
+</note>
+<note lon="-1.4929605" lat="52.4107312">
+  <id>231775</id>
+  <url>http://www.openstreetmap.org/api/0.6/notes/231775</url>
+  <reopen_url>http://www.openstreetmap.org/api/0.6/notes/231775/reopen</reopen_url>
+  <date_created>2014-08-28 19:25:37 UTC</date_created>
+  <status>closed</status>
+  <date_closed>2014-09-27 09:21:41 UTC</date_closed>
+  <comments>
+    <comment>
+      <date>2014-08-28 19:25:37 UTC</date>
+      <uid>1486336</uid>
+      <user>Wyken Seagrave</user>
+      <user_url>http://www.openstreetmap.org/user/Wyken%20Seagrave</user_url>
+      <action>opened</action>
+      <text>Is it Paynes or Payne's</text>
+      <html><p>Is it Paynes or Payne's</p></html>
+    </comment>
+    <comment>
+      <date>2014-09-26 13:05:33 UTC</date>
+      <action>commented</action>
+      <text>Royal Mail's postcode finder has PAYNES LANE</text>
+      <html><p>Royal Mail's postcode finder has PAYNES LANE</p></html>
+    </comment>
+  </comments>
+</note>
+<note lon="-1.4910829" lat="52.4157048">
+  <id>231728</id>
+  <url>http://www.openstreetmap.org/api/0.6/notes/231728</url>
+  <reopen_url>http://www.openstreetmap.org/api/0.6/notes/231728/reopen</reopen_url>
+  <date_created>2014-08-28 18:29:16 UTC</date_created>
+  <status>closed</status>
+  <date_closed>2014-09-27 09:15:46 UTC</date_closed>
+  <comments>
+    <comment>
+      <date>2014-08-28 18:29:16 UTC</date>
+      <uid>1486336</uid>
+      <user>Wyken Seagrave</user>
+      <user_url>http://www.openstreetmap.org/user/Wyken%20Seagrave</user_url>
+      <action>opened</action>
+      <text>Should it be Craner's Road? </text>
+      <html><p>Should it be Craner's Road? </p></html>
+    </comment>
+    <comment>
+      <date>2014-09-26 13:04:15 UTC</date>
+      <action>commented</action>
+      <text>Royal Mail's Postcode finder has CRANERS ROAD</text>
+      <html><p>Royal Mail's Postcode finder has CRANERS ROAD</p></html>
+    </comment>
+    <comment>
+      <date>2014-09-27 09:15:46 UTC</date>
+      <uid>1486336</uid>
+      <user>Wyken Seagrave</user>
+      <user_url>http://www.openstreetmap.org/user/Wyken%20Seagrave</user_url>
+      <action>closed</action>
+      <text>Craners without apostrophe based on survey</text>
+      <html><p>Craners without apostrophe based on survey</p></html>
+    </comment>
+  </comments>
+</note>
+<note lon="-1.4988345" lat="52.3875902">
+  <id>231734</id>
+  <url>http://www.openstreetmap.org/api/0.6/notes/231734</url>
+  <reopen_url>http://www.openstreetmap.org/api/0.6/notes/231734/reopen</reopen_url>
+  <date_created>2014-08-28 18:40:33 UTC</date_created>
+  <status>closed</status>
+  <date_closed>2014-09-27 09:03:23 UTC</date_closed>
+  <comments>
+    <comment>
+      <date>2014-08-28 18:40:33 UTC</date>
+      <uid>1486336</uid>
+      <user>Wyken Seagrave</user>
+      <user_url>http://www.openstreetmap.org/user/Wyken%20Seagrave</user_url>
+      <action>opened</action>
+      <text>Should be Forester's? </text>
+      <html><p>Should be Forester's? </p></html>
+    </comment>
+    <comment>
+      <date>2014-09-26 13:21:59 UTC</date>
+      <action>commented</action>
+      <text>Royal Mail's postcode finder has FORESTERS ROAD</text>
+      <html><p>Royal Mail's postcode finder has FORESTERS ROAD</p></html>
+    </comment>
+    <comment>
+      <date>2014-09-27 09:03:23 UTC</date>
+      <uid>1486336</uid>
+      <user>Wyken Seagrave</user>
+      <user_url>http://www.openstreetmap.org/user/Wyken%20Seagrave</user_url>
+      <action>closed</action>
+      <text>Foresters without apostrophe is correct. Verified by survey.</text>
+      <html><p>Foresters without apostrophe is correct. Verified by survey.</p></html>
+    </comment>
+  </comments>
+</note>
+<note lon="-1.4912814" lat="52.4451356">
+  <id>226232</id>
+  <url>http://www.openstreetmap.org/api/0.6/notes/226232</url>
+  <comment_url>http://www.openstreetmap.org/api/0.6/notes/226232/comment</comment_url>
+  <close_url>http://www.openstreetmap.org/api/0.6/notes/226232/close</close_url>
+  <date_created>2014-08-21 18:28:35 UTC</date_created>
+  <status>open</status>
+  <comments>
+    <comment>
+      <date>2014-08-21 18:28:35 UTC</date>
+      <uid>1486336</uid>
+      <user>Wyken Seagrave</user>
+      <user_url>http://www.openstreetmap.org/user/Wyken%20Seagrave</user_url>
+      <action>opened</action>
+      <text>Is this really called Classic Drive? </text>
+      <html><p>Is this really called Classic Drive? </p></html>
+    </comment>
+  </comments>
+</note>
+<note lon="-1.4849514" lat="52.3966157">
+  <id>141481</id>
+  <url>http://www.openstreetmap.org/api/0.6/notes/141481</url>
+  <comment_url>http://www.openstreetmap.org/api/0.6/notes/141481/comment</comment_url>
+  <close_url>http://www.openstreetmap.org/api/0.6/notes/141481/close</close_url>
+  <date_created>2014-03-29 00:40:00 UTC</date_created>
+  <status>open</status>
+  <comments>
+    <comment>
+      <date>2014-03-29 00:40:00 UTC</date>
+      <action>opened</action>
+      <text>Shared use footpath(s)</text>
+      <html><p>Shared use footpath(s)</p></html>
+    </comment>
+  </comments>
+</note>
+<note lon="-1.4972734" lat="52.4478233">
+  <id>104971</id>
+  <url>http://www.openstreetmap.org/api/0.6/notes/104971</url>
+  <comment_url>http://www.openstreetmap.org/api/0.6/notes/104971/comment</comment_url>
+  <close_url>http://www.openstreetmap.org/api/0.6/notes/104971/close</close_url>
+  <date_created>2014-01-20 11:14:35 UTC</date_created>
+  <status>open</status>
+  <comments>
+    <comment>
+      <date>2014-01-20 11:14:35 UTC</date>
+      <uid>1894030</uid>
+      <user>david halliday</user>
+      <user_url>http://www.openstreetmap.org/user/david%20halliday</user_url>
+      <action>opened</action>
+      <text>Casino. DeVere Hotel. Primary Wine Bar</text>
+      <html><p>Casino. DeVere Hotel. Primary Wine Bar</p></html>
+    </comment>
+  </comments>
+</note>
+<note lon="-1.48541" lat="52.4039375">
+  <id>98185</id>
+  <url>http://www.openstreetmap.org/api/0.6/notes/98185</url>
+  <comment_url>http://www.openstreetmap.org/api/0.6/notes/98185/comment</comment_url>
+  <close_url>http://www.openstreetmap.org/api/0.6/notes/98185/close</close_url>
+  <date_created>2014-01-08 20:51:59 UTC</date_created>
+  <status>open</status>
+  <comments>
+    <comment>
+      <date>2014-01-08 20:51:59 UTC</date>
+      <uid>1874292</uid>
+      <user>CarlRose</user>
+      <user_url>http://www.openstreetmap.org/user/CarlRose</user_url>
+      <action>opened</action>
+      <text>tressler
+</text>
+      <html><p>tressler
+</p></html>
+    </comment>
+    <comment>
+      <date>2014-01-08 20:52:05 UTC</date>
+      <uid>1874292</uid>
+      <user>CarlRose</user>
+      <user_url>http://www.openstreetmap.org/user/CarlRose</user_url>
+      <action>closed</action>
+      <text></text>
+      <html><p></p></html>
+    </comment>
+    <comment>
+      <date>2014-01-08 20:52:19 UTC</date>
+      <uid>1874292</uid>
+      <user>CarlRose</user>
+      <user_url>http://www.openstreetmap.org/user/CarlRose</user_url>
+      <action>reopened</action>
+      <text></text>
+      <html><p></p></html>
+    </comment>
+  </comments>
+</note>
+<note lon="-1.486499" lat="52.4037575">
+  <id>98183</id>
+  <url>http://www.openstreetmap.org/api/0.6/notes/98183</url>
+  <comment_url>http://www.openstreetmap.org/api/0.6/notes/98183/comment</comment_url>
+  <close_url>http://www.openstreetmap.org/api/0.6/notes/98183/close</close_url>
+  <date_created>2014-01-08 20:46:29 UTC</date_created>
+  <status>open</status>
+  <comments>
+    <comment>
+      <date>2014-01-08 20:46:29 UTC</date>
+      <action>opened</action>
+      <text>9 Humber Rd</text>
+      <html><p>9 Humber Rd</p></html>
+    </comment>
+    <comment>
+      <date>2014-01-08 20:51:13 UTC</date>
+      <uid>1874292</uid>
+      <user>CarlRose</user>
+      <user_url>http://www.openstreetmap.org/user/CarlRose</user_url>
+      <action>closed</action>
+      <text></text>
+      <html><p></p></html>
+    </comment>
+    <comment>
+      <date>2014-01-08 20:51:21 UTC</date>
+      <uid>1874292</uid>
+      <user>CarlRose</user>
+      <user_url>http://www.openstreetmap.org/user/CarlRose</user_url>
+      <action>reopened</action>
+      <text></text>
+      <html><p></p></html>
+    </comment>
+  </comments>
+</note>
+<note lon="-1.4832723" lat="51.8736461">
+  <id>97502</id>
+  <url>http://www.openstreetmap.org/api/0.6/notes/97502</url>
+  <comment_url>http://www.openstreetmap.org/api/0.6/notes/97502/comment</comment_url>
+  <close_url>http://www.openstreetmap.org/api/0.6/notes/97502/close</close_url>
+  <date_created>2014-01-07 14:02:05 UTC</date_created>
+  <status>open</status>
+  <comments>
+    <comment>
+      <date>2014-01-07 14:02:05 UTC</date>
+      <action>opened</action>
+      <text>onosm.org submitted note from a business:
+name: Britannick Engineering
+phone: 01608 810332
+website: 
+twitter: 
+hours: 
+category: Factories
+address: Market Street, Charlbury</text>
+      <html><p>onosm.org submitted note from a business:
+<br />name: Britannick Engineering
+<br />phone: 01608 810332
+<br />website: 
+<br />twitter: 
+<br />hours: 
+<br />category: Factories
+<br />address: Market Street, Charlbury</p></html>
+    </comment>
+  </comments>
+</note>
+<note lon="-1.4880681" lat="47.9504144">
+  <id>11430</id>
+  <url>http://www.openstreetmap.org/api/0.6/notes/11430</url>
+  <comment_url>http://www.openstreetmap.org/api/0.6/notes/11430/comment</comment_url>
+  <close_url>http://www.openstreetmap.org/api/0.6/notes/11430/close</close_url>
+  <date_created>2013-07-04 12:08:26 UTC</date_created>
+  <status>open</status>
+  <comments>
+    <comment>
+      <date>2013-07-04 12:08:26 UTC</date>
+      <uid>218616</uid>
+      <user>djakk</user>
+      <user_url>http://www.openstreetmap.org/user/djakk</user_url>
+      <action>opened</action>
+      <text>l'échangeur de Janzé-est sera ouvert en septembre 2013 : un rond-point sera construit au nord de l'échangeur -> à mapper</text>
+      <html><p>l'échangeur de Janzé-est sera ouvert en septembre 2013 : un rond-point sera construit au nord de l'échangeur -&gt; à mapper</p></html>
+    </comment>
+    <comment>
+      <date>2013-11-20 09:48:18 UTC</date>
+      <uid>439947</uid>
+      <user>StephaneP</user>
+      <user_url>http://www.openstreetmap.org/user/StephaneP</user_url>
+      <action>commented</action>
+      <text>Qu'en est-il ? La note est toujours valide ou bien les modifs ont été entrées dans Osm ?</text>
+      <html><p>Qu'en est-il ? La note est toujours valide ou bien les modifs ont été entrées dans Osm ?</p></html>
+    </comment>
+    <comment>
+      <date>2013-11-23 11:22:18 UTC</date>
+      <uid>218616</uid>
+      <user>djakk</user>
+      <user_url>http://www.openstreetmap.org/user/djakk</user_url>
+      <action>commented</action>
+      <text>Coucou, quelqu'un d'autre que moi a mappé et ça semble correct. 
+Reste une aire de covoiturage, qui est sans doute toujours en construction. (Son ouverture n'était pas synchronisée avec celle de l'échangeur). </text>
+      <html><p>Coucou, quelqu'un d'autre que moi a mappé et ça semble correct. 
+<br />Reste une aire de covoiturage, qui est sans doute toujours en construction. (Son ouverture n'était pas synchronisée avec celle de l'échangeur). </p></html>
+    </comment>
+    <comment>
+      <date>2013-12-05 09:13:34 UTC</date>
+      <uid>704348</uid>
+      <user>JBacc1</user>
+      <user_url>http://www.openstreetmap.org/user/JBacc1</user_url>
+      <action>closed</action>
+      <text>Ok, alors on peut fermer.</text>
+      <html><p>Ok, alors on peut fermer.</p></html>
+    </comment>
+    <comment>
+      <date>2013-12-06 19:28:20 UTC</date>
+      <uid>218616</uid>
+      <user>djakk</user>
+      <user_url>http://www.openstreetmap.org/user/djakk</user_url>
+      <action>reopened</action>
+      <text></text>
+      <html><p></p></html>
+    </comment>
+    <comment>
+      <date>2013-12-06 19:28:51 UTC</date>
+      <uid>218616</uid>
+      <user>djakk</user>
+      <user_url>http://www.openstreetmap.org/user/djakk</user_url>
+      <action>commented</action>
+      <text>Non, on va laisser parce qu'il reste l'aire de covoiturage à mapper ;)</text>
+      <html><p>Non, on va laisser parce qu'il reste l'aire de covoiturage à mapper ;)</p></html>
+    </comment>
+  </comments>
+</note>
+<note lon="-1.4872742" lat="52.0666994">
+  <id>81429</id>
+  <url>http://www.openstreetmap.org/api/0.6/notes/81429</url>
+  <comment_url>http://www.openstreetmap.org/api/0.6/notes/81429/comment</comment_url>
+  <close_url>http://www.openstreetmap.org/api/0.6/notes/81429/close</close_url>
+  <date_created>2013-12-01 13:02:25 UTC</date_created>
+  <status>open</status>
+  <comments>
+    <comment>
+      <date>2013-12-01 13:02:25 UTC</date>
+      <action>opened</action>
+      <text>right of way around/through farm poorly marked and needs a re-survey</text>
+      <html><p>right of way around/through farm poorly marked and needs a re-survey</p></html>
+    </comment>
+  </comments>
+</note>
+<note lon="-1.4978313" lat="50.8231801">
+  <id>4918</id>
+  <url>http://www.openstreetmap.org/api/0.6/notes/4918</url>
+  <comment_url>http://www.openstreetmap.org/api/0.6/notes/4918/comment</comment_url>
+  <close_url>http://www.openstreetmap.org/api/0.6/notes/4918/close</close_url>
+  <date_created>2013-05-28 20:31:36 UTC</date_created>
+  <status>open</status>
+  <comments>
+    <comment>
+      <date>2013-05-28 20:31:36 UTC</date>
+      <uid>2098</uid>
+      <user>Andy Street</user>
+      <user_url>http://www.openstreetmap.org/user/Andy%20Street</user_url>
+      <action>opened</action>
+      <text>Overlapping landuse</text>
+      <html><p>Overlapping landuse</p></html>
+    </comment>
+    <comment>
+      <date>2013-06-05 09:24:52 UTC</date>
+      <uid>30587</uid>
+      <user>IknowJoseph</user>
+      <user_url>http://www.openstreetmap.org/user/IknowJoseph</user_url>
+      <action>commented</action>
+      <text>I've created a multipolygon that should take care of the overlapping clearings</text>
+      <html><p>I've created a multipolygon that should take care of the overlapping clearings</p></html>
+    </comment>
+    <comment>
+      <date>2013-06-05 09:26:30 UTC</date>
+      <uid>30587</uid>
+      <user>IknowJoseph</user>
+      <user_url>http://www.openstreetmap.org/user/IknowJoseph</user_url>
+      <action>commented</action>
+      <text>(still more to do though)</text>
+      <html><p>(still more to do though)</p></html>
+    </comment>
+  </comments>
+</note>
+</osm>
+
diff --git a/tests/fixtures/test_NotesSearch.xml b/tests/fixtures/test_NotesSearch.xml
new file mode 100644
index 0000000..fed62ca
--- /dev/null
+++ b/tests/fixtures/test_NotesSearch.xml
@@ -0,0 +1,61 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<osm version="0.6" generator="OpenStreetMap server">
+<note lon="11.96395" lat="57.70301">
+  <id>796</id>
+  <url>http://api06.dev.openstreetmap.org/api/0.6/notes/796</url>
+  <comment_url>http://api06.dev.openstreetmap.org/api/0.6/notes/796/comment</comment_url>
+  <close_url>http://api06.dev.openstreetmap.org/api/0.6/notes/796/close</close_url>
+  <date_created>2014-07-16 16:42:46 UTC</date_created>
+  <status>open</status>
+  <comments>
+    <comment>
+      <date>2014-07-16 16:42:46 UTC</date>
+      <uid>2132</uid>
+      <user>kalaset</user>
+      <user_url>http://master.apis.dev.openstreetmap.org/user/kalaset</user_url>
+      <action>opened</action>
+      <text>One way street:
+jjhghkklll</text>
+      <html><p>One way street:
+<br />jjhghkklll</p></html>
+    </comment>
+  </comments>
+</note>
+<note lon="11.96395" lat="57.70301">
+  <id>788</id>
+  <url>http://api06.dev.openstreetmap.org/api/0.6/notes/788</url>
+  <comment_url>http://api06.dev.openstreetmap.org/api/0.6/notes/788/comment</comment_url>
+  <close_url>http://api06.dev.openstreetmap.org/api/0.6/notes/788/close</close_url>
+  <date_created>2014-07-16 16:12:41 UTC</date_created>
+  <status>open</status>
+  <comments>
+    <comment>
+      <date>2014-07-16 16:12:41 UTC</date>
+      <action>opened</action>
+      <text>One way street:
+comment</text>
+      <html><p>One way street:
+<br />comment</p></html>
+    </comment>
+  </comments>
+</note>
+<note lon="11.96395" lat="57.70301">
+  <id>738</id>
+  <url>http://api06.dev.openstreetmap.org/api/0.6/notes/738</url>
+  <comment_url>http://api06.dev.openstreetmap.org/api/0.6/notes/738/comment</comment_url>
+  <close_url>http://api06.dev.openstreetmap.org/api/0.6/notes/738/close</close_url>
+  <date_created>2014-07-03 12:09:05 UTC</date_created>
+  <status>open</status>
+  <comments>
+    <comment>
+      <date>2014-07-03 12:09:05 UTC</date>
+      <uid>2132</uid>
+      <user>kalaset</user>
+      <user_url>http://master.apis.dev.openstreetmap.org/user/kalaset</user_url>
+      <action>opened</action>
+      <text>One way street tyuui</text>
+      <html><p>One way street tyuui</p></html>
+    </comment>
+  </comments>
+</note>
+</osm>
diff --git a/tests/fixtures/test_RelationCreate.xml b/tests/fixtures/test_RelationCreate.xml
new file mode 100644
index 0000000..3d1ab52
--- /dev/null
+++ b/tests/fixtures/test_RelationCreate.xml
@@ -0,0 +1 @@
+8989
diff --git a/tests/fixtures/test_RelationDelete.xml b/tests/fixtures/test_RelationDelete.xml
new file mode 100644
index 0000000..920a139
--- /dev/null
+++ b/tests/fixtures/test_RelationDelete.xml
@@ -0,0 +1 @@
+43
diff --git a/tests/fixtures/test_RelationFull.xml b/tests/fixtures/test_RelationFull.xml
new file mode 100644
index 0000000..d207ac6
--- /dev/null
+++ b/tests/fixtures/test_RelationFull.xml
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<osm version="0.6" generator="CGImap 0.3.3 (5414 thorn-02.openstreetmap.org)" copyright="OpenStreetMap and contributors" attribution="http://www.openstreetmap.org/copyright" license="http://opendatacommons.org/licenses/odbl/1-0/">
+ <node id="101142276" visible="true" version="8" changeset="762788" timestamp="2009-03-07T22:31:37Z" user="Giorgio Vaccari" uid="92934" lat="41.8825289" lon="12.4904718"/>
+ <node id="101142277" visible="true" version="8" changeset="762788" timestamp="2009-03-07T22:31:37Z" user="Giorgio Vaccari" uid="92934" lat="41.8821839" lon="12.4903002"/>
+ <node id="101142279" visible="true" version="8" changeset="762788" timestamp="2009-03-07T22:31:37Z" user="Giorgio Vaccari" uid="92934" lat="41.8816279" lon="12.4901028"/>
+ <node id="101142360" visible="true" version="8" changeset="762788" timestamp="2009-03-07T22:31:37Z" user="Giorgio Vaccari" uid="92934" lat="41.8818835" lon="12.4901800"/>
+ <node id="255024104" visible="true" version="4" changeset="8698607" timestamp="2011-07-11T22:58:13Z" user="Giardia" uid="113909" lat="41.8828686" lon="12.4903969"/>
+ <node id="246638798" visible="true" version="8" changeset="7531303" timestamp="2011-03-12T09:58:57Z" user="Davio" uid="217070" lat="41.8813915" lon="12.4900957"/>
+ <node id="1198115045" visible="true" version="1" changeset="7531506" timestamp="2011-03-12T10:25:03Z" user="Davio" uid="217070" lat="41.8827424" lon="12.4904489"/>
+ <node id="101142274" visible="true" version="10" changeset="7531506" timestamp="2011-03-12T10:25:28Z" user="Davio" uid="217070" lat="41.8825952" lon="12.4904869"/>
+ <way id="22908029" visible="true" version="17" changeset="14780762" timestamp="2013-01-25T13:35:59Z" user="Davio" uid="217070">
+  <nd ref="101142274"/>
+  <nd ref="101142276"/>
+  <nd ref="101142277"/>
+  <nd ref="101142360"/>
+  <nd ref="101142279"/>
+  <nd ref="246638798"/>
+  <tag k="highway" v="tertiary"/>
+  <tag k="lanes" v="2"/>
+  <tag k="lit" v="yes"/>
+  <tag k="maxspeed" v="50"/>
+  <tag k="name" v="Viale Guido Baccelli"/>
+  <tag k="source:maxspeed" v="IT:urban"/>
+ </way>
+ <way id="190022934" visible="true" version="2" changeset="14780762" timestamp="2013-01-25T13:36:04Z" user="Davio" uid="217070">
+  <nd ref="255024104"/>
+  <nd ref="1198115045"/>
+  <nd ref="101142274"/>
+  <tag k="highway" v="tertiary"/>
+  <tag k="lanes" v="2"/>
+  <tag k="lit" v="yes"/>
+  <tag k="maxspeed" v="50"/>
+  <tag k="oneway" v="yes"/>
+  <tag k="source:maxspeed" v="IT:urban"/>
+ </way>
+ <relation id="2470397" visible="true" version="2" changeset="13819914" timestamp="2012-11-10T12:15:40Z" user="Davio" uid="217070">
+  <member type="way" ref="22908029" role="to"/>
+  <member type="node" ref="101142274" role="via"/>
+  <member type="way" ref="190022934" role="from"/>
+  <tag k="restriction" v="only_straight_on"/>
+  <tag k="type" v="restriction"/>
+ </relation>
+</osm>
diff --git a/tests/fixtures/test_RelationGet.xml b/tests/fixtures/test_RelationGet.xml
new file mode 100644
index 0000000..42a15ed
--- /dev/null
+++ b/tests/fixtures/test_RelationGet.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<osm version="0.6" generator="OpenStreetMap server" copyright="OpenStreetMap and contributors" attribution="http://www.openstreetmap.org/copyright" license="http://opendatacommons.org/licenses/odbl/1-0/">
+  <relation id="321" changeset="434" timestamp="2009-09-15T22:24:25Z" version="1" visible="true" user="green525" uid="12">
+    <member type="way" ref="6908" role="outer"/>
+    <member type="way" ref="6352" role="outer"/>
+    <member type="way" ref="5669" role="outer"/>
+    <member type="way" ref="5682" role="outer"/>
+    <member type="way" ref="6909" role="outer"/>
+    <member type="way" ref="6355" role="outer"/>
+    <member type="way" ref="6910" role="outer"/>
+    <member type="way" ref="6911" role="outer"/>
+    <member type="way" ref="6912" role="outer"/>
+    <tag k="admin_level" v="9"/>
+    <tag k="boundary" v="administrative"/>
+    <tag k="type" v="multipolygon"/>
+  </relation>
+</osm>
diff --git a/tests/fixtures/test_RelationGet_with_version.xml b/tests/fixtures/test_RelationGet_with_version.xml
new file mode 100644
index 0000000..12b395b
--- /dev/null
+++ b/tests/fixtures/test_RelationGet_with_version.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<osm version="0.6" generator="OpenStreetMap server" copyright="OpenStreetMap and contributors" attribution="http://www.openstreetmap.org/copyright" license="http://opendatacommons.org/licenses/odbl/1-0/">
+  <relation id="765" changeset="41378" timestamp="2014-04-26T08:19:23Z" version="2" visible="true" user="metaodi" uid="1841">
+    <member type="way" ref="9902" role="outer"/>
+    <member type="way" ref="9903" role="outer"/>
+    <member type="way" ref="8428" role="outer"/>
+    <member type="way" ref="9904" role="outer"/>
+    <member type="way" ref="8440" role="outer"/>
+    <member type="way" ref="8362" role="outer"/>
+    <member type="way" ref="8455" role="outer"/>
+    <member type="way" ref="8460" role="outer"/>
+    <tag k="admin_level" v="9"/>
+    <tag k="alt_name" v="M\xf5ksi"/>
+    <tag k="boundary" v="administrative"/>
+    <tag k="EHAK:code" v="5141"/>
+    <tag k="EHAK:countycode" v="0086"/>
+    <tag k="EHAK:parishcode" v="0918"/>
+    <tag k="is_in" v="V\xf5ru vald"/>
+    <tag k="name" v="M\xf5ksi k\xfcla"/>
+    <tag k="source" v="test"/>
+    <tag k="type" v="multipolygon"/>
+  </relation>
+</osm>
diff --git a/tests/fixtures/test_RelationHistory.xml b/tests/fixtures/test_RelationHistory.xml
new file mode 100644
index 0000000..9ff445d
--- /dev/null
+++ b/tests/fixtures/test_RelationHistory.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<osm version="0.6" generator="OpenStreetMap server" copyright="OpenStreetMap and contributors" attribution="http://www.openstreetmap.org/copyright" license="http://opendatacommons.org/licenses/odbl/1-0/">
+  <relation id="2470397" changeset="13458460" timestamp="2012-10-11T19:13:34Z" version="1" visible="true" user="Davio" uid="217070">
+    <member type="way" ref="22908055" role="from"/>
+    <member type="way" ref="22908029" role="to"/>
+    <member type="node" ref="101142274" role="via"/>
+    <tag k="restriction" v="only_straight_on"/>
+    <tag k="type" v="restriction"/>
+  </relation>
+  <relation id="2470397" changeset="13819914" timestamp="2012-11-10T12:15:40Z" version="2" visible="true" user="Davio" uid="217070">
+    <member type="way" ref="22908029" role="to"/>
+    <member type="node" ref="101142274" role="via"/>
+    <member type="way" ref="190022934" role="from"/>
+    <tag k="restriction" v="only_straight_on"/>
+    <tag k="type" v="restriction"/>
+  </relation>
+</osm>
diff --git a/tests/fixtures/test_RelationRelations.xml b/tests/fixtures/test_RelationRelations.xml
new file mode 100644
index 0000000..9a287b1
--- /dev/null
+++ b/tests/fixtures/test_RelationRelations.xml
@@ -0,0 +1,131 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<osm version="0.6" generator="OpenStreetMap server" copyright="OpenStreetMap and contributors" attribution="http://www.openstreetmap.org/copyright" license="http://opendatacommons.org/licenses/odbl/1-0/">
+  <relation id="1532553" changeset="18454175" timestamp="2013-10-20T16:01:47Z" version="85" visible="true" user="SimonPoole" uid="92387">
+    <member type="relation" ref="1532552" role=""/>
+    <member type="relation" ref="1606999" role=""/>
+    <member type="relation" ref="1607116" role=""/>
+    <member type="relation" ref="1610062" role=""/>
+    <member type="relation" ref="1610061" role=""/>
+    <member type="relation" ref="1613231" role=""/>
+    <member type="relation" ref="1614159" role=""/>
+    <member type="relation" ref="1614158" role=""/>
+    <member type="relation" ref="1614160" role=""/>
+    <member type="relation" ref="1617756" role=""/>
+    <member type="relation" ref="1618548" role=""/>
+    <member type="relation" ref="1634226" role=""/>
+    <member type="relation" ref="1634225" role=""/>
+    <member type="relation" ref="1639907" role=""/>
+    <member type="relation" ref="1641195" role=""/>
+    <member type="relation" ref="1641194" role=""/>
+    <member type="relation" ref="1641196" role=""/>
+    <member type="relation" ref="279555" role=""/>
+    <member type="relation" ref="279553" role=""/>
+    <member type="relation" ref="1614157" role=""/>
+    <member type="relation" ref="1641602" role=""/>
+    <member type="relation" ref="1641650" role=""/>
+    <member type="relation" ref="1643000" role=""/>
+    <member type="relation" ref="1643618" role=""/>
+    <member type="relation" ref="1096838" role=""/>
+    <member type="relation" ref="1643188" role=""/>
+    <member type="relation" ref="1648018" role=""/>
+    <member type="relation" ref="1648017" role=""/>
+    <member type="relation" ref="1648016" role=""/>
+    <member type="relation" ref="1648019" role=""/>
+    <member type="relation" ref="1648459" role=""/>
+    <member type="relation" ref="1650069" role=""/>
+    <member type="relation" ref="1650068" role=""/>
+    <member type="relation" ref="1650070" role=""/>
+    <member type="relation" ref="1652328" role=""/>
+    <member type="relation" ref="1652327" role=""/>
+    <member type="relation" ref="1652798" role=""/>
+    <member type="relation" ref="1660012" role=""/>
+    <member type="relation" ref="1660011" role=""/>
+    <member type="relation" ref="1660317" role=""/>
+    <member type="relation" ref="1660437" role=""/>
+    <member type="relation" ref="1661348" role=""/>
+    <member type="relation" ref="1661349" role=""/>
+    <member type="relation" ref="1661876" role=""/>
+    <member type="relation" ref="1671748" role=""/>
+    <member type="relation" ref="1671749" role=""/>
+    <member type="relation" ref="1672454" role=""/>
+    <member type="relation" ref="1699561" role=""/>
+    <member type="relation" ref="1699562" role=""/>
+    <member type="relation" ref="1700657" role=""/>
+    <member type="relation" ref="1701185" role=""/>
+    <member type="relation" ref="1702851" role=""/>
+    <member type="relation" ref="1703058" role=""/>
+    <member type="relation" ref="1648328" role=""/>
+    <member type="relation" ref="1707267" role=""/>
+    <member type="relation" ref="1707268" role=""/>
+    <member type="relation" ref="1707266" role=""/>
+    <member type="relation" ref="1707473" role=""/>
+    <member type="relation" ref="1709233" role=""/>
+    <member type="relation" ref="1709232" role=""/>
+    <member type="relation" ref="1709235" role=""/>
+    <member type="relation" ref="1709234" role=""/>
+    <member type="relation" ref="1709277" role=""/>
+    <member type="relation" ref="1717428" role=""/>
+    <member type="relation" ref="1717427" role=""/>
+    <member type="relation" ref="1720898" role=""/>
+    <member type="relation" ref="1727739" role=""/>
+    <member type="relation" ref="1753487" role=""/>
+    <member type="relation" ref="1753488" role=""/>
+    <member type="relation" ref="1753779" role=""/>
+    <member type="relation" ref="1754046" role=""/>
+    <member type="relation" ref="1754045" role=""/>
+    <member type="relation" ref="1767088" role=""/>
+    <member type="relation" ref="1767087" role=""/>
+    <member type="relation" ref="1769915" role=""/>
+    <member type="relation" ref="1769914" role=""/>
+    <member type="relation" ref="1801975" role=""/>
+    <member type="relation" ref="1802012" role=""/>
+    <member type="relation" ref="1804055" role=""/>
+    <member type="relation" ref="1812014" role=""/>
+    <member type="relation" ref="1818473" role=""/>
+    <member type="relation" ref="2110480" role=""/>
+    <member type="relation" ref="2110900" role=""/>
+    <member type="relation" ref="2110901" role=""/>
+    <member type="relation" ref="2110902" role=""/>
+    <member type="relation" ref="2114006" role=""/>
+    <member type="relation" ref="2114005" role=""/>
+    <member type="relation" ref="2115555" role=""/>
+    <member type="relation" ref="2115556" role=""/>
+    <member type="relation" ref="2127232" role=""/>
+    <member type="relation" ref="2127212" role=""/>
+    <member type="relation" ref="2127908" role=""/>
+    <member type="relation" ref="2128588" role=""/>
+    <member type="relation" ref="2142303" role=""/>
+    <member type="relation" ref="2142304" role=""/>
+    <member type="relation" ref="2142305" role=""/>
+    <member type="relation" ref="2143678" role=""/>
+    <member type="relation" ref="2143677" role=""/>
+    <member type="relation" ref="2156061" role=""/>
+    <member type="relation" ref="2156062" role=""/>
+    <member type="relation" ref="2184794" role=""/>
+    <member type="relation" ref="2188159" role=""/>
+    <member type="relation" ref="2188974" role=""/>
+    <member type="relation" ref="2195373" role=""/>
+    <member type="relation" ref="2195655" role=""/>
+    <member type="relation" ref="2195656" role=""/>
+    <member type="relation" ref="2196573" role=""/>
+    <member type="relation" ref="2200173" role=""/>
+    <member type="relation" ref="2200260" role=""/>
+    <member type="relation" ref="2200731" role=""/>
+    <member type="relation" ref="2201598" role=""/>
+    <member type="relation" ref="2202921" role=""/>
+    <member type="relation" ref="2202920" role=""/>
+    <member type="relation" ref="2224989" role=""/>
+    <member type="relation" ref="2236735" role=""/>
+    <member type="relation" ref="2909831" role=""/>
+    <member type="relation" ref="2909830" role=""/>
+    <member type="relation" ref="3006087" role=""/>
+    <member type="relation" ref="3097585" role=""/>
+    <member type="relation" ref="3282415" role=""/>
+    <tag k="name" v="Aargauischer Radroutennetz"/>
+    <tag k="network" v="lcn"/>
+    <tag k="note" v="we are waiting for documentation on the actual routes"/>
+    <tag k="operator" v="Kanton Aargau"/>
+    <tag k="type" v="network"/>
+    <tag k="url" v="https://www.ag.ch/de/bvu/mobilitaet_verkehr/langsamverkehr/zweiradverkehr_1/kantonale_radrouten_1.jsp"/>
+  </relation>
+</osm>
diff --git a/tests/fixtures/test_RelationUpdate.xml b/tests/fixtures/test_RelationUpdate.xml
new file mode 100644
index 0000000..d81cc07
--- /dev/null
+++ b/tests/fixtures/test_RelationUpdate.xml
@@ -0,0 +1 @@
+42
diff --git a/tests/fixtures/test_RelationsGet.xml b/tests/fixtures/test_RelationsGet.xml
new file mode 100644
index 0000000..880ac09
--- /dev/null
+++ b/tests/fixtures/test_RelationsGet.xml
@@ -0,0 +1,157 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<osm version="0.6" generator="CGImap 0.3.3 (29596 thorn-03.openstreetmap.org)" copyright="OpenStreetMap and contributors" attribution="http://www.openstreetmap.org/copyright" license="http://opendatacommons.org/licenses/odbl/1-0/">
+ <relation id="1532552" visible="true" version="12" changeset="15643945" timestamp="2013-04-07T17:36:35Z" user="SimonPoole" uid="92387">
+  <member type="way" ref="107733849" role=""/>
+  <member type="way" ref="36791523" role=""/>
+  <member type="way" ref="107736660" role=""/>
+  <member type="way" ref="52000399" role=""/>
+  <member type="way" ref="170793678" role=""/>
+  <member type="way" ref="107736662" role=""/>
+  <member type="way" ref="49216948" role=""/>
+  <member type="way" ref="27009606" role=""/>
+  <member type="way" ref="47977728" role=""/>
+  <member type="way" ref="47977726" role=""/>
+  <member type="way" ref="35479117" role=""/>
+  <member type="way" ref="35479116" role=""/>
+  <member type="way" ref="119104101" role=""/>
+  <member type="way" ref="214323580" role=""/>
+  <member type="way" ref="104364414" role=""/>
+  <member type="way" ref="35479103" role=""/>
+  <member type="way" ref="35479119" role=""/>
+  <member type="way" ref="104364411" role=""/>
+  <member type="way" ref="36827858" role=""/>
+  <tag k="name" v="Widen - Dietikon"/>
+  <tag k="network" v="lcn"/>
+  <tag k="ref" v="ARRN"/>
+  <tag k="route" v="bicycle"/>
+  <tag k="type" v="route"/>
+ </relation>
+ <relation id="1532553" visible="true" version="85" changeset="18454175" timestamp="2013-10-20T16:01:47Z" user="SimonPoole" uid="92387">
+  <member type="relation" ref="1532552" role=""/>
+  <member type="relation" ref="1606999" role=""/>
+  <member type="relation" ref="1607116" role=""/>
+  <member type="relation" ref="1610062" role=""/>
+  <member type="relation" ref="1610061" role=""/>
+  <member type="relation" ref="1613231" role=""/>
+  <member type="relation" ref="1614159" role=""/>
+  <member type="relation" ref="1614158" role=""/>
+  <member type="relation" ref="1614160" role=""/>
+  <member type="relation" ref="1617756" role=""/>
+  <member type="relation" ref="1618548" role=""/>
+  <member type="relation" ref="1634226" role=""/>
+  <member type="relation" ref="1634225" role=""/>
+  <member type="relation" ref="1639907" role=""/>
+  <member type="relation" ref="1641195" role=""/>
+  <member type="relation" ref="1641194" role=""/>
+  <member type="relation" ref="1641196" role=""/>
+  <member type="relation" ref="279555" role=""/>
+  <member type="relation" ref="279553" role=""/>
+  <member type="relation" ref="1614157" role=""/>
+  <member type="relation" ref="1641602" role=""/>
+  <member type="relation" ref="1641650" role=""/>
+  <member type="relation" ref="1643000" role=""/>
+  <member type="relation" ref="1643618" role=""/>
+  <member type="relation" ref="1096838" role=""/>
+  <member type="relation" ref="1643188" role=""/>
+  <member type="relation" ref="1648018" role=""/>
+  <member type="relation" ref="1648017" role=""/>
+  <member type="relation" ref="1648016" role=""/>
+  <member type="relation" ref="1648019" role=""/>
+  <member type="relation" ref="1648459" role=""/>
+  <member type="relation" ref="1650069" role=""/>
+  <member type="relation" ref="1650068" role=""/>
+  <member type="relation" ref="1650070" role=""/>
+  <member type="relation" ref="1652328" role=""/>
+  <member type="relation" ref="1652327" role=""/>
+  <member type="relation" ref="1652798" role=""/>
+  <member type="relation" ref="1660012" role=""/>
+  <member type="relation" ref="1660011" role=""/>
+  <member type="relation" ref="1660317" role=""/>
+  <member type="relation" ref="1660437" role=""/>
+  <member type="relation" ref="1661348" role=""/>
+  <member type="relation" ref="1661349" role=""/>
+  <member type="relation" ref="1661876" role=""/>
+  <member type="relation" ref="1671748" role=""/>
+  <member type="relation" ref="1671749" role=""/>
+  <member type="relation" ref="1672454" role=""/>
+  <member type="relation" ref="1699561" role=""/>
+  <member type="relation" ref="1699562" role=""/>
+  <member type="relation" ref="1700657" role=""/>
+  <member type="relation" ref="1701185" role=""/>
+  <member type="relation" ref="1702851" role=""/>
+  <member type="relation" ref="1703058" role=""/>
+  <member type="relation" ref="1648328" role=""/>
+  <member type="relation" ref="1707267" role=""/>
+  <member type="relation" ref="1707268" role=""/>
+  <member type="relation" ref="1707266" role=""/>
+  <member type="relation" ref="1707473" role=""/>
+  <member type="relation" ref="1709233" role=""/>
+  <member type="relation" ref="1709232" role=""/>
+  <member type="relation" ref="1709235" role=""/>
+  <member type="relation" ref="1709234" role=""/>
+  <member type="relation" ref="1709277" role=""/>
+  <member type="relation" ref="1717428" role=""/>
+  <member type="relation" ref="1717427" role=""/>
+  <member type="relation" ref="1720898" role=""/>
+  <member type="relation" ref="1727739" role=""/>
+  <member type="relation" ref="1753487" role=""/>
+  <member type="relation" ref="1753488" role=""/>
+  <member type="relation" ref="1753779" role=""/>
+  <member type="relation" ref="1754046" role=""/>
+  <member type="relation" ref="1754045" role=""/>
+  <member type="relation" ref="1767088" role=""/>
+  <member type="relation" ref="1767087" role=""/>
+  <member type="relation" ref="1769915" role=""/>
+  <member type="relation" ref="1769914" role=""/>
+  <member type="relation" ref="1801975" role=""/>
+  <member type="relation" ref="1802012" role=""/>
+  <member type="relation" ref="1804055" role=""/>
+  <member type="relation" ref="1812014" role=""/>
+  <member type="relation" ref="1818473" role=""/>
+  <member type="relation" ref="2110480" role=""/>
+  <member type="relation" ref="2110900" role=""/>
+  <member type="relation" ref="2110901" role=""/>
+  <member type="relation" ref="2110902" role=""/>
+  <member type="relation" ref="2114006" role=""/>
+  <member type="relation" ref="2114005" role=""/>
+  <member type="relation" ref="2115555" role=""/>
+  <member type="relation" ref="2115556" role=""/>
+  <member type="relation" ref="2127232" role=""/>
+  <member type="relation" ref="2127212" role=""/>
+  <member type="relation" ref="2127908" role=""/>
+  <member type="relation" ref="2128588" role=""/>
+  <member type="relation" ref="2142303" role=""/>
+  <member type="relation" ref="2142304" role=""/>
+  <member type="relation" ref="2142305" role=""/>
+  <member type="relation" ref="2143678" role=""/>
+  <member type="relation" ref="2143677" role=""/>
+  <member type="relation" ref="2156061" role=""/>
+  <member type="relation" ref="2156062" role=""/>
+  <member type="relation" ref="2184794" role=""/>
+  <member type="relation" ref="2188159" role=""/>
+  <member type="relation" ref="2188974" role=""/>
+  <member type="relation" ref="2195373" role=""/>
+  <member type="relation" ref="2195655" role=""/>
+  <member type="relation" ref="2195656" role=""/>
+  <member type="relation" ref="2196573" role=""/>
+  <member type="relation" ref="2200173" role=""/>
+  <member type="relation" ref="2200260" role=""/>
+  <member type="relation" ref="2200731" role=""/>
+  <member type="relation" ref="2201598" role=""/>
+  <member type="relation" ref="2202921" role=""/>
+  <member type="relation" ref="2202920" role=""/>
+  <member type="relation" ref="2224989" role=""/>
+  <member type="relation" ref="2236735" role=""/>
+  <member type="relation" ref="2909831" role=""/>
+  <member type="relation" ref="2909830" role=""/>
+  <member type="relation" ref="3006087" role=""/>
+  <member type="relation" ref="3097585" role=""/>
+  <member type="relation" ref="3282415" role=""/>
+  <tag k="name" v="Aargauischer Radroutennetz"/>
+  <tag k="network" v="lcn"/>
+  <tag k="note" v="we are waiting for documentation on the actual routes"/>
+  <tag k="operator" v="Kanton Aargau"/>
+  <tag k="type" v="network"/>
+  <tag k="url" v="https://www.ag.ch/de/bvu/mobilitaet_verkehr/langsamverkehr/zweiradverkehr_1/kantonale_radrouten_1.jsp"/>
+ </relation>
+</osm>
diff --git a/tests/fixtures/test_WayCreate.xml b/tests/fixtures/test_WayCreate.xml
new file mode 100644
index 0000000..af7b560
--- /dev/null
+++ b/tests/fixtures/test_WayCreate.xml
@@ -0,0 +1 @@
+5454
diff --git a/tests/fixtures/test_WayDelete.xml b/tests/fixtures/test_WayDelete.xml
new file mode 100644
index 0000000..45a4fb7
--- /dev/null
+++ b/tests/fixtures/test_WayDelete.xml
@@ -0,0 +1 @@
+8
diff --git a/tests/fixtures/test_WayFull.xml b/tests/fixtures/test_WayFull.xml
new file mode 100644
index 0000000..5ada7e4
--- /dev/null
+++ b/tests/fixtures/test_WayFull.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<osm version="0.6" generator="OpenStreetMap server" copyright="OpenStreetMap and contributors" attribution="http://www.openstreetmap.org/copyright" license="http://opendatacommons.org/licenses/odbl/1-0/">
+  <node id="11949" changeset="298" timestamp="2009-09-14T23:23:17Z" version="1" visible="true" user="green525" uid="12" lat="58.415759" lon="23.0982328"/>
+  <node id="11950" changeset="298" timestamp="2009-09-14T23:23:17Z" version="1" visible="true" user="green525" uid="12" lat="58.4156975" lon="23.0984681"/>
+  <node id="11951" changeset="298" timestamp="2009-09-14T23:23:17Z" version="1" visible="true" user="green525" uid="12" lat="58.4156196" lon="23.0987147"/>
+  <node id="11952" changeset="298" timestamp="2009-09-14T23:23:17Z" version="1" visible="true" user="green525" uid="12" lat="58.4154641" lon="23.0988164"/>
+  <node id="11953" changeset="298" timestamp="2009-09-14T23:23:17Z" version="1" visible="true" user="green525" uid="12" lat="58.4153527" lon="23.0988175"/>
+  <node id="11954" changeset="298" timestamp="2009-09-14T23:23:17Z" version="1" visible="true" user="green525" uid="12" lat="58.4152182" lon="23.0988145"/>
+  <node id="11955" changeset="298" timestamp="2009-09-14T23:23:17Z" version="1" visible="true" user="green525" uid="12" lat="58.4151147" lon="23.0987825"/>
+  <node id="11956" changeset="298" timestamp="2009-09-14T23:23:17Z" version="1" visible="true" user="green525" uid="12" lat="58.4150599" lon="23.0987376"/>
+  <node id="11957" changeset="298" timestamp="2009-09-14T23:23:18Z" version="1" visible="true" user="green525" uid="12" lat="58.4151643" lon="23.0986577"/>
+  <node id="11958" changeset="298" timestamp="2009-09-14T23:23:18Z" version="1" visible="true" user="green525" uid="12" lat="58.4153457" lon="23.098516"/>
+  <node id="11959" changeset="298" timestamp="2009-09-14T23:23:18Z" version="1" visible="true" user="green525" uid="12" lat="58.415344" lon="23.0983088"/>
+  <node id="11960" changeset="298" timestamp="2009-09-14T23:23:18Z" version="1" visible="true" user="green525" uid="12" lat="58.4153569" lon="23.0980949"/>
+  <node id="11961" changeset="298" timestamp="2009-09-14T23:23:18Z" version="1" visible="true" user="green525" uid="12" lat="58.4153767" lon="23.0980277"/>
+  <node id="11962" changeset="298" timestamp="2009-09-14T23:23:18Z" version="1" visible="true" user="green525" uid="12" lat="58.4154623" lon="23.0980434"/>
+  <node id="11963" changeset="298" timestamp="2009-09-14T23:23:18Z" version="1" visible="true" user="green525" uid="12" lat="58.4155678" lon="23.0980445"/>
+  <node id="11964" changeset="298" timestamp="2009-09-14T23:23:18Z" version="1" visible="true" user="green525" uid="12" lat="58.4156796" lon="23.098086"/>
+  <way id="321" changeset="298" timestamp="2009-09-14T23:23:18Z" version="1" visible="true" user="green525" uid="12">
+    <nd ref="11949"/>
+    <nd ref="11950"/>
+    <nd ref="11951"/>
+    <nd ref="11952"/>
+    <nd ref="11953"/>
+    <nd ref="11954"/>
+    <nd ref="11955"/>
+    <nd ref="11956"/>
+    <nd ref="11957"/>
+    <nd ref="11958"/>
+    <nd ref="11959"/>
+    <nd ref="11960"/>
+    <nd ref="11961"/>
+    <nd ref="11962"/>
+    <nd ref="11963"/>
+    <nd ref="11964"/>
+    <nd ref="11949"/>
+    <tag k="admin_level" v="9"/>
+    <tag k="boundary" v="administrative"/>
+    <tag k="source" v="Maa-amet 01.06.2009"/>
+  </way>
+</osm>
diff --git a/tests/fixtures/test_WayGet.xml b/tests/fixtures/test_WayGet.xml
new file mode 100644
index 0000000..2cf615a
--- /dev/null
+++ b/tests/fixtures/test_WayGet.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<osm version="0.6" generator="OpenStreetMap server" copyright="OpenStreetMap and contributors" attribution="http://www.openstreetmap.org/copyright" license="http://opendatacommons.org/licenses/odbl/1-0/">
+  <way id="321" changeset="298" timestamp="2009-09-14T23:23:18Z" version="1" visible="true" user="green525" uid="12">
+    <nd ref="11949"/>
+    <nd ref="11950"/>
+    <nd ref="11951"/>
+    <nd ref="11952"/>
+    <nd ref="11953"/>
+    <nd ref="11954"/>
+    <nd ref="11955"/>
+    <nd ref="11956"/>
+    <nd ref="11957"/>
+    <nd ref="11958"/>
+    <nd ref="11959"/>
+    <nd ref="11960"/>
+    <nd ref="11961"/>
+    <nd ref="11962"/>
+    <nd ref="11963"/>
+    <nd ref="11964"/>
+    <nd ref="11949"/>
+    <tag k="admin_level" v="9"/>
+    <tag k="boundary" v="administrative"/>
+  </way>
+</osm>
diff --git a/tests/fixtures/test_WayGet_nodata.xml b/tests/fixtures/test_WayGet_nodata.xml
new file mode 100644
index 0000000..e69de29
diff --git a/tests/fixtures/test_WayGet_with_version.xml b/tests/fixtures/test_WayGet_with_version.xml
new file mode 100644
index 0000000..a5a3886
--- /dev/null
+++ b/tests/fixtures/test_WayGet_with_version.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<osm version="0.6" generator="OpenStreetMap server" copyright="OpenStreetMap and contributors" attribution="http://www.openstreetmap.org/copyright" license="http://opendatacommons.org/licenses/odbl/1-0/">
+  <way id="4294967296" changeset="41303" timestamp="2014-04-23T15:02:04Z" version="2" visible="true" user="metaodi" uid="1841">
+    <nd ref="4295832773"/>
+    <nd ref="4294967304"/>
+    <nd ref="4294967303"/>
+    <nd ref="4294967300"/>
+    <nd ref="4608751"/>
+    <nd ref="4294967305"/>
+    <nd ref="4294967302"/>
+    <nd ref="8548430"/>
+    <nd ref="4294967296"/>
+    <nd ref="4294967301"/>
+    <nd ref="4294967298"/>
+    <nd ref="4294967306"/>
+    <nd ref="7855737"/>
+    <nd ref="4294967297"/>
+    <nd ref="4294967299"/>
+    <tag k="highway" v="unclassified"/>
+    <tag k="name" v="Stansted Road"/>
+  </way>
+</osm>
diff --git a/tests/fixtures/test_WayHistory.xml b/tests/fixtures/test_WayHistory.xml
new file mode 100644
index 0000000..20b8d05
--- /dev/null
+++ b/tests/fixtures/test_WayHistory.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<osm version="0.6" generator="OpenStreetMap server" copyright="OpenStreetMap and contributors" attribution="http://www.openstreetmap.org/copyright" license="http://opendatacommons.org/licenses/odbl/1-0/">
+  <way id="4294967296" changeset="16947" timestamp="2012-12-30T18:45:12Z" version="1" visible="true" user="TomH" uid="15">
+    <nd ref="4294967304"/>
+    <nd ref="4294967303"/>
+    <nd ref="4294967300"/>
+    <nd ref="4608751"/>
+    <nd ref="4294967305"/>
+    <nd ref="4294967302"/>
+    <nd ref="8548430"/>
+    <nd ref="4294967296"/>
+    <nd ref="4294967301"/>
+    <nd ref="4294967298"/>
+    <nd ref="4294967306"/>
+    <nd ref="7855737"/>
+    <nd ref="4294967297"/>
+    <nd ref="4294967299"/>
+    <tag k="highway" v="unclassified"/>
+    <tag k="name" v="Stansted Road"/>
+  </way>
+  <way id="4294967296" changeset="41303" timestamp="2014-04-23T15:02:04Z" version="2" visible="true" user="metaodi" uid="1841">
+    <nd ref="4295832773"/>
+    <nd ref="4294967304"/>
+    <nd ref="4294967303"/>
+    <nd ref="4294967300"/>
+    <nd ref="4608751"/>
+    <nd ref="4294967305"/>
+    <nd ref="4294967302"/>
+    <nd ref="8548430"/>
+    <nd ref="4294967296"/>
+    <nd ref="4294967301"/>
+    <nd ref="4294967298"/>
+    <nd ref="4294967306"/>
+    <nd ref="7855737"/>
+    <nd ref="4294967297"/>
+    <nd ref="4294967299"/>
+    <tag k="highway" v="unclassified"/>
+    <tag k="name" v="Stansted Road"/>
+  </way>
+</osm>
diff --git a/tests/fixtures/test_WayRelations.xml b/tests/fixtures/test_WayRelations.xml
new file mode 100644
index 0000000..aba6a8f
--- /dev/null
+++ b/tests/fixtures/test_WayRelations.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<osm version="0.6" generator="OpenStreetMap server" copyright="OpenStreetMap and contributors" attribution="http://www.openstreetmap.org/copyright" license="http://opendatacommons.org/licenses/odbl/1-0/">
+  <relation id="4294968148" changeset="23123" timestamp="2013-05-14T10:33:05Z" version="1" visible="true" user="tyrTester06" uid="1178">
+    <member type="way" ref="4295032195" role="line"/>
+    <member type="node" ref="4295668179" role="point"/>
+    <member type="node" ref="4295668178" role=""/>
+    <member type="way" ref="4295032194" role=""/>
+    <member type="way" ref="4295032193" role=""/>
+    <member type="node" ref="4295668174" role="foo"/>
+    <member type="node" ref="4295668175" role="bar"/>
+    <tag k="type" v="fancy"/>
+  </relation>
+</osm>
diff --git a/tests/fixtures/test_WayUpdate.xml b/tests/fixtures/test_WayUpdate.xml
new file mode 100644
index 0000000..7f8f011
--- /dev/null
+++ b/tests/fixtures/test_WayUpdate.xml
@@ -0,0 +1 @@
+7
diff --git a/tests/fixtures/test_WaysGet.xml b/tests/fixtures/test_WaysGet.xml
new file mode 100644
index 0000000..dcfc27e
--- /dev/null
+++ b/tests/fixtures/test_WaysGet.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<osm version="0.6" generator="OpenStreetMap server" copyright="OpenStreetMap and contributors" attribution="http://www.openstreetmap.org/copyright" license="http://opendatacommons.org/licenses/odbl/1-0/">
+  <way id="456" changeset="299" timestamp="2009-09-14T23:29:05Z" version="1" visible="true" user="green525" uid="12">
+    <nd ref="17267"/>
+    <nd ref="17293"/>
+    <nd ref="16650"/>
+    <tag k="admin_level" v="8"/>
+    <tag k="boundary" v="administrative"/>
+    <tag k="source" v="Maa-amet 01.06.2009"/>
+  </way>
+  <way id="678" changeset="305" timestamp="2009-09-15T00:24:36Z" version="1" visible="true" user="green525" uid="12">
+    <nd ref="81603"/>
+    <nd ref="81604"/>
+    <nd ref="81605"/>
+    <nd ref="81606"/>
+    <nd ref="81607"/>
+    <nd ref="81608"/>
+    <nd ref="81609"/>
+    <nd ref="81610"/>
+    <nd ref="81611"/>
+    <nd ref="81612"/>
+    <nd ref="81613"/>
+    <nd ref="81614"/>
+    <nd ref="81615"/>
+    <nd ref="81616"/>
+    <nd ref="81603"/>
+    <tag k="admin_level" v="9"/>
+    <tag k="boundary" v="administrative"/>
+    <tag k="source" v="Maa-amet 01.06.2009"/>
+  </way>
+</osm>
diff --git a/tests/helper_tests.py b/tests/helper_tests.py
new file mode 100644
index 0000000..cfc528b
--- /dev/null
+++ b/tests/helper_tests.py
@@ -0,0 +1,117 @@
+from __future__ import (unicode_literals, absolute_import)
+from nose.tools import *  # noqa
+from . import osmapi_tests
+import osmapi
+import mock
+
+
+class TestOsmApiHelper(osmapi_tests.TestOsmApi):
+    def setUp(self):
+        super(TestOsmApiHelper, self).setUp()
+        self.setupMock()
+
+    def setupMock(self, status=200):
+        mock_response = mock.Mock()
+        mock_response.status = status
+        mock_response.reason = "test reason"
+        mock_response.read = mock.Mock(return_value='test response')
+        self.api._conn.getresponse = mock.Mock(return_value=mock_response)
+        self.api._conn.putrequest = mock.Mock()
+        self.api._conn.putheader = mock.Mock()
+        self.api._conn.send = mock.Mock()
+        self.api._conn.endheaders = mock.Mock()
+        self.api._username = 'testuser'
+        self.api._password = 'testpassword'
+
+    def test_http_request_get(self):
+        response = self.api._http_request(
+            'GET',
+            '/api/0.6/test',
+            False,
+            None
+        )
+        self.api._conn.putrequest.assert_called_with('GET', '/api/0.6/test')
+        self.assertEquals(response, "test response")
+        self.assertEquals(self.api._conn.putheader.call_count, 1)
+
+    def test_http_request_put(self):
+        response = self.api._http_request(
+            'PUT',
+            '/api/0.6/testput',
+            False,
+            "data"
+        )
+        self.api._conn.putrequest.assert_called_with(
+            'PUT',
+            '/api/0.6/testput'
+        )
+        self.assertEquals(response, "test response")
+        self.assertEquals(self.api._conn.putheader.call_count, 2)
+        args = self.api._conn.putheader.call_args_list[1]
+        header, value = args[0]
+        self.assertEquals(header, 'Content-Length')
+        self.assertEquals(value, 4)
+
+    def test_http_request_delete(self):
+        response = self.api._http_request(
+            'PUT',
+            '/api/0.6/testdelete',
+            False,
+            "delete data"
+        )
+        self.api._conn.putrequest.assert_called_with(
+            'PUT',
+            '/api/0.6/testdelete'
+        )
+        self.assertEquals(response, "test response")
+        self.assertEquals(self.api._conn.putheader.call_count, 2)
+        args = self.api._conn.putheader.call_args_list[1]
+        header, value = args[0]
+        self.assertEquals(header, 'Content-Length')
+        self.assertEquals(value, 11)
+
+    def test_http_request_auth(self):
+        response = self.api._http_request(
+            'PUT',
+            '/api/0.6/testauth',
+            True,
+            None
+        )
+        self.api._conn.putrequest.assert_called_with(
+            'PUT',
+            '/api/0.6/testauth'
+        )
+        self.assertEquals(response, "test response")
+
+        self.assertEquals(self.api._conn.putheader.call_count, 2)
+        args = self.api._conn.putheader.call_args_list[1]
+        header, value = args[0]
+        self.assertEquals(header, 'Authorization')
+        self.assertEquals(value, 'Basic dGVzdHVzZXI6dGVzdHBhc3N3b3Jk')
+
+    def test_http_request_410_response(self):
+        self.setupMock(410)
+        response = self.api._http_request(
+            'GET',
+            '/api/0.6/test410',
+            False,
+            None
+        )
+        self.api._conn.putrequest.assert_called_with(
+            'GET',
+            '/api/0.6/test410'
+        )
+        self.assertIsNone(response, "test response")
+
+    def test_http_request_500_response(self):
+        self.setupMock(500)
+        with self.assertRaises(osmapi.ApiError) as cm:
+            self.api._http_request(
+                'GET',
+                '/api/0.6/test500',
+                False,
+                None
+            )
+        self.assertEquals(cm.exception.status, 500)
+        self.assertEquals(cm.exception.reason, "test reason")
+        self.assertEquals(cm.exception.payload, "test response")
diff --git a/tests/node_tests.py b/tests/node_tests.py
new file mode 100644
index 0000000..b435c70
--- /dev/null
+++ b/tests/node_tests.py
@@ -0,0 +1,317 @@
+from __future__ import (unicode_literals, absolute_import)
+from nose.tools import *  # noqa
+from . import osmapi_tests
+from osmapi import OsmApi, UsernamePasswordMissingError
+import mock
+import datetime
+
+
+class TestOsmApiNode(osmapi_tests.TestOsmApi):
+    def test_NodeGet(self):
+        self._conn_mock()
+
+        result = self.api.NodeGet(123)
+
+        args, kwargs = self.api._conn.putrequest.call_args
+        self.assertEquals(args[0], 'GET')
+        self.assertEquals(args[1], '/api/0.6/node/123')
+
+        self.assertEquals(result, {
+            'id': 123,
+            'changeset': 15293,
+            'uid': 605,
+            'timestamp': datetime.datetime(2012, 4, 18, 11, 14, 26),
+            'lat': 51.8753146,
+            'lon': -1.4857118,
+            'visible': True,
+            'version': 8,
+            'user': 'freundchen',
+            'tag': {
+                'amenity': 'school',
+                'foo': 'bar',
+                'name': 'Berolina & Schule'
+            },
+        })
+
+    def test_NodeGet_with_version(self):
+        self._conn_mock()
+
+        result = self.api.NodeGet(123, NodeVersion=2)
+
+        args, kwargs = self.api._conn.putrequest.call_args
+        self.assertEquals(args[0], 'GET')
+        self.assertEquals(args[1], '/api/0.6/node/123/2')
+
+        self.assertEquals(result, {
+            'id': 123,
+            'changeset': 4152,
+            'uid': 605,
+            'timestamp': datetime.datetime(2011, 4, 18, 11, 14, 26),
+            'lat': 51.8753146,
+            'lon': -1.4857118,
+            'visible': True,
+            'version': 2,
+            'user': 'freundchen',
+            'tag': {
+                'amenity': 'school',
+            },
+        })
+
+    def test_NodeCreate_changesetauto(self):
+        # setup mock
+        self.api = OsmApi(
+            api="api06.dev.openstreetmap.org",
+            changesetauto=True
+        )
+        self._conn_mock(auth=True, filenames=[
+            'test_NodeCreate_changesetauto.xml',
+            'test_ChangesetUpload_create_node.xml',
+            'test_ChangesetClose.xml',
+        ])
+
+        test_node = {
+            'lat': 47.123,
+            'lon': 8.555,
+            'tag': {
+                'amenity': 'place_of_worship',
+                'religion': 'pastafarian'
+            }
+        }
+
+        self.assertIsNone(self.api.NodeCreate(test_node))
+
+    def test_NodeCreate(self):
+        self._conn_mock(auth=True)
+
+        # setup mock
+        self.api.ChangesetCreate = mock.Mock(
+            return_value=1111
+        )
+        self.api._CurrentChangesetId = 1111
+
+        test_node = {
+            'lat': 47.287,
+            'lon': 8.765,
+            'tag': {
+                'amenity': 'place_of_worship',
+                'religion': 'pastafarian'
+            }
+        }
+
+        cs = self.api.ChangesetCreate({
+            'comment': 'This is a test dataset'
+        })
+        self.assertEquals(cs, 1111)
+        result = self.api.NodeCreate(test_node)
+
+        args, kwargs = self.api._conn.putrequest.call_args
+        self.assertEquals(args[0], 'PUT')
+        self.assertEquals(args[1], '/api/0.6/node/create')
+
+        self.assertEquals(result['id'], 9876)
+        self.assertEquals(result['lat'], test_node['lat'])
+        self.assertEquals(result['lon'], test_node['lon'])
+        self.assertEquals(result['tag'], test_node['tag'])
+
+    def test_NodeCreate_wo_changeset(self):
+        test_node = {
+            'lat': 47.287,
+            'lon': 8.765,
+            'tag': {
+                'amenity': 'place_of_worship',
+                'religion': 'pastafarian'
+            }
+        }
+
+        with self.assertRaisesRegexp(
+                Exception,
+                'need to open a changeset'):
+            self.api.NodeCreate(test_node)
+
+    def test_NodeCreate_wo_auth(self):
+        self._conn_mock()
+
+        # setup mock
+        self.api.ChangesetCreate = mock.Mock(
+            return_value=1111
+        )
+        self.api._CurrentChangesetId = 1111
+        test_node = {
+            'lat': 47.287,
+            'lon': 8.765,
+            'tag': {
+                'amenity': 'place_of_worship',
+                'religion': 'pastafarian'
+            }
+        }
+
+        with self.assertRaisesRegexp(
+                UsernamePasswordMissingError,
+                'Username/Password missing'):
+            self.api.NodeCreate(test_node)
+
+    def test_NodeUpdate(self):
+        self._conn_mock(auth=True)
+
+        # setup mock
+        self.api.ChangesetCreate = mock.Mock(
+            return_value=1111
+        )
+        self.api._CurrentChangesetId = 1111
+
+        test_node = {
+            'id': 7676,
+            'lat': 47.287,
+            'lon': 8.765,
+            'tag': {
+                'amenity': 'place_of_worship',
+                'name': 'christian'
+            }
+        }
+
+        cs = self.api.ChangesetCreate({
+            'comment': 'This is a test dataset'
+        })
+        self.assertEquals(cs, 1111)
+        result = self.api.NodeUpdate(test_node)
+
+        args, kwargs = self.api._conn.putrequest.call_args
+        self.assertEquals(args[0], 'PUT')
+        self.assertEquals(args[1], '/api/0.6/node/7676')
+
+        self.assertEquals(result['id'], 7676)
+        self.assertEquals(result['version'], 3)
+        self.assertEquals(result['lat'], test_node['lat'])
+        self.assertEquals(result['lon'], test_node['lon'])
+        self.assertEquals(result['tag'], test_node['tag'])
+
+    def test_NodeDelete(self):
+        self._conn_mock(auth=True)
+
+        # setup mock
+        self.api.ChangesetCreate = mock.Mock(
+            return_value=1111
+        )
+        self.api._CurrentChangesetId = 1111
+
+        test_node = {
+            'id': 7676
+        }
+
+        cs = self.api.ChangesetCreate({
+            'comment': 'This is a test dataset'
+        })
+        self.assertEquals(cs, 1111)
+
+        result = self.api.NodeDelete(test_node)
+
+        args, kwargs = self.api._conn.putrequest.call_args
+        self.assertEquals(args[0], 'DELETE')
+        self.assertEquals(args[1], '/api/0.6/node/7676')
+        self.assertEquals(result['id'], 7676)
+        self.assertEquals(result['version'], 4)
+
+    def test_NodeHistory(self):
+        self._conn_mock()
+
+        result = self.api.NodeHistory(123)
+
+        args, kwargs = self.api._conn.putrequest.call_args
+        self.assertEquals(args[0], 'GET')
+        self.assertEquals(args[1], '/api/0.6/node/123/history')
+
+        self.assertEquals(len(result), 8)
+        self.assertEquals(result[4]['id'], 123)
+        self.assertEquals(result[4]['version'], 4)
+        self.assertEquals(result[4]['lat'], 51.8753146)
+        self.assertEquals(result[4]['lon'], -1.4857118)
+        self.assertEquals(
+            result[4]['tag'], {
+                'empty': '',
+                'foo': 'bar',
+            }
+        )
+
+    def test_NodeWays(self):
+        self._conn_mock()
+
+        result = self.api.NodeWays(234)
+
+        args, kwargs = self.api._conn.putrequest.call_args
+        self.assertEquals(args[0], 'GET')
+        self.assertEquals(args[1], '/api/0.6/node/234/ways')
+
+        self.assertEquals(len(result), 1)
+        self.assertEquals(result[0]['id'], 60)
+        self.assertEquals(result[0]['changeset'], 61)
+        self.assertEquals(
+            result[0]['tag'],
+            {
+                'highway': 'path',
+                'name': 'Dog walking path',
+            }
+        )
+
+    def test_NodeRelations(self):
+        self._conn_mock()
+
+        result = self.api.NodeRelations(4295668179)
+
+        args, kwargs = self.api._conn.putrequest.call_args
+        self.assertEquals(args[0], 'GET')
+        self.assertEquals(args[1], '/api/0.6/node/4295668179/relations')
+
+        self.assertEquals(len(result), 1)
+        self.assertEquals(result[0]['id'], 4294968148)
+        self.assertEquals(result[0]['changeset'], 23123)
+        self.assertEquals(
+            result[0]['member'][1],
+            {
+                'role': 'point',
+                'ref': 4295668179,
+                'type': 'node',
+            }
+        )
+        self.assertEquals(
+            result[0]['tag'],
+            {
+                'type': 'fancy',
+            }
+        )
+
+    def test_NodesGet(self):
+        self._conn_mock()
+
+        result = self.api.NodesGet([123, 345])
+
+        args, kwargs = self.api._conn.putrequest.call_args
+        self.assertEquals(args[0], 'GET')
+        self.assertEquals(args[1], '/api/0.6/nodes?nodes=123,345')
+
+        self.assertEquals(len(result), 2)
+        self.assertEquals(result[123], {
+            'id': 123,
+            'changeset': 15293,
+            'uid': 605,
+            'timestamp': datetime.datetime(2012, 4, 18, 11, 14, 26),
+            'lat': 51.8753146,
+            'lon': -1.4857118,
+            'visible': True,
+            'version': 8,
+            'user': 'freundchen',
+            'tag': {
+                'amenity': 'school',
+                'foo': 'bar',
+                'name': 'Berolina & Schule'
+            },
+        })
+        self.assertEquals(result[345], {
+            'id': 345,
+            'changeset': 244,
+            'timestamp': datetime.datetime(2009, 9, 12, 3, 22, 59),
+            'uid': 1,
+            'visible': False,
+            'version': 2,
+            'user': 'guggis',
+            'tag': {},
+        })
diff --git a/tests/notes_tests.py b/tests/notes_tests.py
new file mode 100644
index 0000000..d6121e3
--- /dev/null
+++ b/tests/notes_tests.py
@@ -0,0 +1,375 @@
+from __future__ import (unicode_literals, absolute_import)
+from nose.tools import *  # noqa
+from . import osmapi_tests
+from datetime import datetime
+
+try:
+    import urlparse
+except ImportError:
+    from urllib import parse as urlparse
+
+
+class TestOsmApiNotes(osmapi_tests.TestOsmApi):
+    def test_NotesGet(self):
+        self._conn_mock()
+
+        result = self.api.NotesGet(
+            -1.4998534,
+            45.9667901,
+            -1.4831815,
+            52.4710193
+        )
+
+        args, kwargs = self.api._conn.putrequest.call_args
+        self.assertEquals(args[0], 'GET')
+
+        urlParts = urlparse.urlparse(args[1])
+        params = urlparse.parse_qs(urlParts.query)
+        self.assertEquals(
+            params['bbox'][0],
+            '-1.499853,45.966790,-1.483181,52.471019'
+        )
+        self.assertEquals(params['limit'][0], '100')
+        self.assertEquals(params['closed'][0], '7')
+
+        self.assertEquals(len(result), 14)
+        self.assertEquals(result[2], {
+            'id': '231775',
+            'lon': -1.4929605,
+            'lat': 52.4107312,
+            'date_created': datetime(2014, 8, 28, 19, 25, 37),
+            'date_closed': datetime(2014, 9, 27, 9, 21, 41),
+            'status': 'closed',
+            'comments': [
+                {
+                    'date': datetime(2014, 8, 28, 19, 25, 37),
+                    'action': 'opened',
+                    'text': "Is it Paynes or Payne's",
+                    'html': "<p>Is it Paynes or Payne's</p>",
+                    'uid': '1486336',
+                    'user': 'Wyken Seagrave'
+                },
+                {
+                    'date': datetime(2014, 9, 26, 13, 5, 33),
+                    'action': 'commented',
+                    'text': "Royal Mail's postcode finder has PAYNES LANE",
+                    'html':
+                    (
+                        "<p>Royal Mail's postcode finder "
+                        "has PAYNES LANE</p>"
+                    ),
+                    'uid': None,
+                    'user': None
+                }
+            ]
+        })
+
+    def test_NoteGet(self):
+        self._conn_mock()
+
+        result = self.api.NoteGet(1111)
+
+        args, kwargs = self.api._conn.putrequest.call_args
+        self.assertEquals(args[0], 'GET')
+        self.assertEquals(args[1], '/api/0.6/notes/1111')
+
+        self.assertEquals(result, {
+            'id': '1111',
+            'lon': 12.3133135,
+            'lat': 37.9305489,
+            'date_created': datetime(2013, 5, 1, 20, 58, 21),
+            'date_closed': datetime(2013, 8, 21, 16, 43, 26),
+            'status': 'closed',
+            'comments': [
+                {
+                    'date': datetime(2013, 5, 1, 20, 58, 21),
+                    'action': 'opened',
+                    'text': "It does not exist this path",
+                    'html': "<p>It does not exist this path</p>",
+                    'uid': '1363438',
+                    'user': 'giuseppemari'
+                },
+                {
+                    'date': datetime(2013, 8, 21, 16, 43, 26),
+                    'action': 'closed',
+                    'text': "there is no path signed",
+                    'html': "<p>there is no path signed</p>",
+                    'uid': '1714220',
+                    'user': 'luschi'
+                }
+            ]
+        })
+
+    def test_NoteCreate(self):
+        self._conn_mock(auth=True)
+
+        note = {
+            'lat': 47.123,
+            'lon': 8.432,
+            'text': 'This is a test'
+        }
+        result = self.api.NoteCreate(note)
+
+        args, kwargs = self.api._conn.putrequest.call_args
+        self.assertEquals(args[0], 'POST')
+
+        urlParts = urlparse.urlparse(args[1])
+        params = urlparse.parse_qs(urlParts.query)
+        self.assertEquals(params['lat'][0], '47.123')
+        self.assertEquals(params['lon'][0], '8.432')
+        self.assertEquals(params['text'][0], 'This is a test')
+
+        self.assertEquals(result, {
+            'id': '816',
+            'lat': 47.123,
+            'lon': 8.432,
+            'date_created': datetime(2014, 10, 3, 15, 21, 21),
+            'date_closed': None,
+            'status': 'open',
+            'comments': [
+                {
+                    'date': datetime(2014, 10, 3, 15, 21, 22),
+                    'action': 'opened',
+                    'text': "This is a test",
+                    'html': "<p>This is a test</p>",
+                    'uid': '1841',
+                    'user': 'metaodi'
+                }
+            ]
+        })
+
+    def test_NoteCreateAnonymous(self):
+        self._conn_mock()
+
+        note = {
+            'lat': 47.123,
+            'lon': 8.432,
+            'text': 'test 123'
+        }
+        result = self.api.NoteCreate(note)
+
+        args, kwargs = self.api._conn.putrequest.call_args
+        self.assertEquals(args[0], 'POST')
+
+        urlParts = urlparse.urlparse(args[1])
+        params = urlparse.parse_qs(urlParts.query)
+        self.assertEquals(params['lat'][0], '47.123')
+        self.assertEquals(params['lon'][0], '8.432')
+        self.assertEquals(params['text'][0], 'test 123')
+
+        self.assertEquals(result, {
+            'id': '842',
+            'lat': 58.3368222,
+            'lon': 25.8826183,
+            'date_created': datetime(2015, 1, 3, 10, 49, 39),
+            'date_closed': None,
+            'status': 'open',
+            'comments': [
+                {
+                    'date': datetime(2015, 1, 3, 10, 49, 39),
+                    'action': 'opened',
+                    'text': "test 123",
+                    'html': "<p>test 123</p>",
+                    'uid': None,
+                    'user': None,
+                }
+            ]
+        })
+
+    def test_NoteComment(self):
+        self._conn_mock(auth=True)
+
+        result = self.api.NoteComment(812, 'This is a comment')
+
+        args, kwargs = self.api._conn.putrequest.call_args
+        self.assertEquals(args[0], 'POST')
+        self.assertEquals(
+            args[1],
+            '/api/0.6/notes/812/comment?text=This+is+a+comment'
+        )
+
+        self.assertEquals(result, {
+            'id': '812',
+            'lat': 47.123,
+            'lon': 8.432,
+            'date_created': datetime(2014, 10, 3, 15, 11, 5),
+            'date_closed': None,
+            'status': 'open',
+            'comments': [
+                {
+                    'date': datetime(2014, 10, 3, 15, 11, 5),
+                    'action': 'opened',
+                    'text': "This is a test",
+                    'html': "<p>This is a test</p>",
+                    'uid': '1841',
+                    'user': 'metaodi'
+                },
+                {
+                    'date': datetime(2014, 10, 4, 22, 36, 35),
+                    'action': 'commented',
+                    'text': "This is a comment",
+                    'html': "<p>This is a comment</p>",
+                    'uid': '1841',
+                    'user': 'metaodi'
+                }
+            ]
+        })
+
+    def test_NoteCommentAnonymous(self):
+        self._conn_mock()
+
+        result = self.api.NoteComment(842, 'blubb')
+
+        args, kwargs = self.api._conn.putrequest.call_args
+        self.assertEquals(args[0], 'POST')
+        self.assertEquals(
+            args[1],
+            '/api/0.6/notes/842/comment?text=blubb'
+        )
+
+        self.assertEquals(result, {
+            'id': '842',
+            'lat': 58.3368222,
+            'lon': 25.8826183,
+            'date_created': datetime(2015, 1, 3, 10, 49, 39),
+            'date_closed': None,
+            'status': 'open',
+            'comments': [
+                {
+                    'date': datetime(2015, 1, 3, 10, 49, 39),
+                    'action': 'opened',
+                    'text': "test 123",
+                    'html': "<p>test 123</p>",
+                    'uid': None,
+                    'user': None,
+                },
+                {
+                    'date': datetime(2015, 1, 3, 11, 6, 0),
+                    'action': 'commented',
+                    'text': "blubb",
+                    'html': "<p>blubb</p>",
+                    'uid': None,
+                    'user': None,
+                }
+            ]
+        })
+
+    def test_NoteClose(self):
+        self._conn_mock(auth=True)
+
+        result = self.api.NoteClose(814, 'Close this note!')
+
+        args, kwargs = self.api._conn.putrequest.call_args
+        self.assertEquals(args[0], 'POST')
+        self.assertEquals(
+            args[1],
+            '/api/0.6/notes/814/close?text=Close+this+note%21'
+        )
+
+        self.assertEquals(result, {
+            'id': '815',
+            'lat': 47.123,
+            'lon': 8.432,
+            'date_created': datetime(2014, 10, 3, 15, 20, 57),
+            'date_closed': datetime(2014, 10, 5, 16, 35, 13),
+            'status': 'closed',
+            'comments': [
+                {
+                    'date': datetime(2014, 10, 3, 15, 20, 57),
+                    'action': 'opened',
+                    'text': "This is a test",
+                    'html': "<p>This is a test</p>",
+                    'uid': '1841',
+                    'user': 'metaodi'
+                },
+                {
+                    'date': datetime(2014, 10, 5, 16, 35, 13),
+                    'action': 'closed',
+                    'text': "Close this note!",
+                    'html': "<p>Close this note!</p>",
+                    'uid': '1841',
+                    'user': 'metaodi'
+                }
+            ]
+        })
+
+    def test_NoteReopen(self):
+        self._conn_mock(auth=True)
+
+        result = self.api.NoteReopen(815, 'Reopen this note!')
+
+        args, kwargs = self.api._conn.putrequest.call_args
+        self.assertEquals(args[0], 'POST')
+        self.assertEquals(
+            args[1],
+            '/api/0.6/notes/815/reopen?text=Reopen+this+note%21'
+        )
+
+        self.assertEquals(result, {
+            'id': '815',
+            'lat': 47.123,
+            'lon': 8.432,
+            'date_created': datetime(2014, 10, 3, 15, 20, 57),
+            'date_closed': None,
+            'status': 'open',
+            'comments': [
+                {
+                    'date': datetime(2014, 10, 3, 15, 20, 57),
+                    'action': 'opened',
+                    'text': "This is a test",
+                    'html': "<p>This is a test</p>",
+                    'uid': '1841',
+                    'user': 'metaodi'
+                },
+                {
+                    'date': datetime(2014, 10, 5, 16, 35, 13),
+                    'action': 'closed',
+                    'text': "Close this note!",
+                    'html': "<p>Close this note!</p>",
+                    'uid': '1841',
+                    'user': 'metaodi'
+                },
+                {
+                    'date': datetime(2014, 10, 5, 16, 44, 56),
+                    'action': 'reopened',
+                    'text': "Reopen this note!",
+                    'html': "<p>Reopen this note!</p>",
+                    'uid': '1841',
+                    'user': 'metaodi'
+                }
+            ]
+        })
+
+    def test_NotesSearch(self):
+        self._conn_mock()
+
+        result = self.api.NotesSearch('street')
+
+        args, kwargs = self.api._conn.putrequest.call_args
+        self.assertEquals(args[0], 'GET')
+
+        urlParts = urlparse.urlparse(args[1])
+        params = urlparse.parse_qs(urlParts.query)
+        self.assertEquals(params['q'][0], 'street')
+        self.assertEquals(params['limit'][0], '100')
+        self.assertEquals(params['closed'][0], '7')
+
+        self.assertEquals(len(result), 3)
+        self.assertEquals(result[1], {
+            'id': '788',
+            'lon': 11.96395,
+            'lat': 57.70301,
+            'date_created': datetime(2014, 7, 16, 16, 12, 41),
+            'date_closed': None,
+            'status': 'open',
+            'comments': [
+                {
+                    'date': datetime(2014, 7, 16, 16, 12, 41),
+                    'action': 'opened',
+                    'text': "One way street:\ncomment",
+                    'html': "<p>One way street:\n<br />comment</p>",
+                    'uid': None,
+                    'user': None
+                }
+            ]
+        })
diff --git a/tests/osmapi_tests.py b/tests/osmapi_tests.py
new file mode 100644
index 0000000..e6e5e4a
--- /dev/null
+++ b/tests/osmapi_tests.py
@@ -0,0 +1,76 @@
+from __future__ import unicode_literals
+from nose.tools import *  # noqa
+from osmapi import OsmApi
+import mock
+import os
+import sys
+
+if sys.version_info < (2, 7):
+    import unittest2 as unittest
+else:
+    import unittest
+
+__location__ = os.path.realpath(
+    os.path.join(
+        os.getcwd(),
+        os.path.dirname(__file__)
+    )
+)
+
+
+class TestOsmApi(unittest.TestCase):
+    def setUp(self):
+        self.api = OsmApi(
+            api="api06.dev.openstreetmap.org"
+        )
+        self.maxDiff = None
+        print(self._testMethodName)
+        print(self.api)
+
+    def _conn_mock(self, auth=False, filenames=None, status=200, reason=None):
+        if auth:
+            self.api._username = 'testuser'
+            self.api._password = 'testpassword'
+
+        response_mock = mock.Mock()
+        response_mock.status = status
+        response_mock.reason = reason
+        response_mock.read = mock.Mock(
+            side_effect=self._return_values(filenames)
+        )
+
+        conn_mock = mock.Mock()
+        conn_mock.putrequest = mock.Mock()
+        conn_mock.putheader = mock.Mock()
+        conn_mock.endheaders = mock.Mock()
+        conn_mock.send = mock.Mock()
+        conn_mock.getresponse = mock.Mock(return_value=response_mock)
+
+        self.api._get_http_connection = mock.Mock(return_value=conn_mock)
+        self.api._conn = conn_mock
+
+        self.api._sleep = mock.Mock()
+
+    def _return_values(self, filenames):
+        if filenames is None:
+            filenames = [self._testMethodName + ".xml"]
+
+        return_values = []
+        for filename in filenames:
+            path = os.path.join(
+                __location__,
+                'fixtures',
+                filename
+            )
+            try:
+                with open(path) as file:
+                    return_values.append(file.read())
+            except:
+                pass
+        return return_values
+
+    def teardown(self):
+        pass
+
+    def test_constructor(self):
+        assert_true(isinstance(self.api, OsmApi))
diff --git a/tests/relation_tests.py b/tests/relation_tests.py
new file mode 100644
index 0000000..82b85ad
--- /dev/null
+++ b/tests/relation_tests.py
@@ -0,0 +1,283 @@
+from __future__ import (unicode_literals, absolute_import)
+from nose.tools import *  # noqa
+from . import osmapi_tests
+import mock
+import datetime
+
+
+def debug(result):
+    from pprint import pprint
+    pprint(result)
+    assert_equals(0, 1)
+
+
+class TestOsmApiRelation(osmapi_tests.TestOsmApi):
+    def test_RelationGet(self):
+        self._conn_mock()
+
+        result = self.api.RelationGet(321)
+
+        args, kwargs = self.api._conn.putrequest.call_args
+        self.assertEquals(args[0], 'GET')
+        self.assertEquals(args[1], '/api/0.6/relation/321')
+
+        self.assertEquals(result, {
+            'id': 321,
+            'changeset': 434,
+            'uid': 12,
+            'timestamp': datetime.datetime(2009, 9, 15, 22, 24, 25),
+            'visible': True,
+            'version': 1,
+            'user': 'green525',
+            'tag': {
+                'admin_level': '9',
+                'boundary': 'administrative',
+                'type': 'multipolygon',
+            },
+            'member': [
+                {
+                    'ref': 6908,
+                    'role': 'outer',
+                    'type': 'way'
+                },
+                {
+                    'ref': 6352,
+                    'role': 'outer',
+                    'type': 'way'
+                },
+                {
+                    'ref': 5669,
+                    'role': 'outer',
+                    'type': 'way'
+                },
+                {
+                    'ref': 5682,
+                    'role': 'outer',
+                    'type': 'way'
+                },
+                {
+                    'ref': 6909,
+                    'role': 'outer',
+                    'type': 'way'
+                },
+                {
+                    'ref': 6355,
+                    'role': 'outer',
+                    'type': 'way'
+                },
+                {
+                    'ref': 6910,
+                    'role': 'outer',
+                    'type': 'way'
+                },
+                {
+                    'ref': 6911,
+                    'role': 'outer',
+                    'type': 'way'
+                },
+                {
+                    'ref': 6912,
+                    'role': 'outer',
+                    'type': 'way'
+                }
+            ]
+        })
+
+    def test_RelationGet_with_version(self):
+        self._conn_mock()
+
+        result = self.api.RelationGet(765, 2)
+
+        args, kwargs = self.api._conn.putrequest.call_args
+        self.assertEquals(args[0], 'GET')
+        self.assertEquals(args[1], '/api/0.6/relation/765/2')
+
+        self.assertEquals(result['id'], 765)
+        self.assertEquals(result['changeset'], 41378)
+        self.assertEquals(result['user'], 'metaodi')
+        self.assertEquals(result['tag']['source'], 'test')
+
+    def test_RelationCreate(self):
+        self._conn_mock(auth=True)
+
+        # setup mock
+        self.api.ChangesetCreate = mock.Mock(
+            return_value=3333
+        )
+        self.api._CurrentChangesetId = 3333
+
+        test_relation = {
+            'tag': {
+                'type': 'test',
+            },
+            'member': [
+                {
+                    'ref': 6908,
+                    'role': 'outer',
+                    'type': 'way'
+                },
+                {
+                    'ref': 6352,
+                    'role': 'point',
+                    'type': 'node'
+                },
+            ]
+        }
+
+        cs = self.api.ChangesetCreate({
+            'comment': 'This is a test relation'
+        })
+        self.assertEquals(cs, 3333)
+
+        result = self.api.RelationCreate(test_relation)
+
+        args, kwargs = self.api._conn.putrequest.call_args
+        self.assertEquals(args[0], 'PUT')
+        self.assertEquals(args[1], '/api/0.6/relation/create')
+
+        self.assertEquals(result['id'], 8989)
+        self.assertEquals(result['version'], 1)
+        self.assertEquals(result['member'], test_relation['member'])
+        self.assertEquals(result['tag'], test_relation['tag'])
+
+    def test_RelationUpdate(self):
+        self._conn_mock(auth=True)
+
+        # setup mock
+        self.api.ChangesetCreate = mock.Mock(
+            return_value=3333
+        )
+        self.api._CurrentChangesetId = 3333
+
+        test_relation = {
+            'id': 8989,
+            'tag': {
+                'type': 'test update',
+            },
+            'member': [
+                {
+                    'ref': 6908,
+                    'role': 'outer',
+                    'type': 'way'
+                }
+            ]
+        }
+
+        cs = self.api.ChangesetCreate({
+            'comment': 'This is a test relation'
+        })
+        self.assertEquals(cs, 3333)
+
+        result = self.api.RelationUpdate(test_relation)
+
+        args, kwargs = self.api._conn.putrequest.call_args
+        self.assertEquals(args[0], 'PUT')
+        self.assertEquals(args[1], '/api/0.6/relation/8989')
+
+        self.assertEquals(result['id'], 8989)
+        self.assertEquals(result['version'], 42)
+        self.assertEquals(result['member'], test_relation['member'])
+        self.assertEquals(result['tag'], test_relation['tag'])
+
+    def test_RelationDelete(self):
+        self._conn_mock(auth=True)
+
+        # setup mock
+        self.api.ChangesetCreate = mock.Mock(
+            return_value=3333
+        )
+        self.api._CurrentChangesetId = 3333
+
+        test_relation = {
+            'id': 8989
+        }
+
+        cs = self.api.ChangesetCreate({
+            'comment': 'This is a test relation'
+        })
+        self.assertEquals(cs, 3333)
+
+        result = self.api.RelationDelete(test_relation)
+
+        args, kwargs = self.api._conn.putrequest.call_args
+        self.assertEquals(args[0], 'DELETE')
+        self.assertEquals(args[1], '/api/0.6/relation/8989')
+
+        self.assertEquals(result['id'], 8989)
+        self.assertEquals(result['version'], 43)
+
+    def test_RelationHistory(self):
+        self._conn_mock()
+
+        result = self.api.RelationHistory(2470397)
+
+        args, kwargs = self.api._conn.putrequest.call_args
+        self.assertEquals(args[0], 'GET')
+        self.assertEquals(args[1], '/api/0.6/relation/2470397/history')
+
+        self.assertEquals(len(result), 2)
+        self.assertEquals(result[1]['id'], 2470397)
+        self.assertEquals(result[1]['version'], 1)
+        self.assertEquals(
+            result[1]['tag'], {
+                'restriction': 'only_straight_on',
+                'type': 'restriction',
+            }
+        )
+        self.assertEquals(result[2]['version'], 2)
+
+    def test_RelationRelations(self):
+        self._conn_mock()
+
+        result = self.api.RelationRelations(1532552)
+
+        args, kwargs = self.api._conn.putrequest.call_args
+        self.assertEquals(args[0], 'GET')
+        self.assertEquals(args[1], '/api/0.6/relation/1532552/relations')
+
+        self.assertEquals(len(result), 1)
+        self.assertEquals(result[0]['id'], 1532553)
+        self.assertEquals(result[0]['version'], 85)
+        self.assertEquals(len(result[0]['member']), 120)
+        self.assertEquals(result[0]['tag']['type'], 'network')
+        self.assertEquals(
+            result[0]['tag']['name'],
+            'Aargauischer Radroutennetz'
+        )
+
+    def test_RelationFull(self):
+        self._conn_mock()
+
+        result = self.api.RelationFull(2470397)
+
+        args, kwargs = self.api._conn.putrequest.call_args
+        self.assertEquals(args[0], 'GET')
+        self.assertEquals(args[1], '/api/0.6/relation/2470397/full')
+
+        self.assertEquals(len(result), 11)
+        self.assertEquals(result[1]['data']['id'], 101142277)
+        self.assertEquals(result[1]['data']['version'], 8)
+        self.assertEquals(result[1]['type'], 'node')
+        self.assertEquals(result[10]['data']['id'], 2470397)
+        self.assertEquals(result[10]['data']['version'], 2)
+        self.assertEquals(result[10]['type'], 'relation')
+
+    def test_RelationsGet(self):
+        self._conn_mock()
+
+        result = self.api.RelationsGet([1532552, 1532553])
+
+        args, kwargs = self.api._conn.putrequest.call_args
+        self.assertEquals(args[0], 'GET')
+        self.assertEquals(
+            args[1],
+            '/api/0.6/relations?relations=1532552,1532553'
+        )
+
+        self.assertEquals(len(result), 2)
+        self.assertEquals(result[1532553]['id'], 1532553)
+        self.assertEquals(result[1532553]['version'], 85)
+        self.assertEquals(result[1532553]['user'], 'SimonPoole')
+        self.assertEquals(result[1532552]['id'], 1532552)
+        self.assertEquals(result[1532552]['visible'], True)
+        self.assertEquals(result[1532552]['tag']['route'], 'bicycle')
diff --git a/tests/way_tests.py b/tests/way_tests.py
new file mode 100644
index 0000000..233d249
--- /dev/null
+++ b/tests/way_tests.py
@@ -0,0 +1,243 @@
+from __future__ import (unicode_literals, absolute_import)
+from nose.tools import *  # noqa
+from . import osmapi_tests
+import mock
+import datetime
+
+
+class TestOsmApiWay(osmapi_tests.TestOsmApi):
+    def test_WayGet(self):
+        self._conn_mock()
+
+        result = self.api.WayGet(321)
+
+        args, kwargs = self.api._conn.putrequest.call_args
+        self.assertEquals(args[0], 'GET')
+        self.assertEquals(args[1], '/api/0.6/way/321')
+
+        self.assertEquals(result, {
+            'id': 321,
+            'changeset': 298,
+            'uid': 12,
+            'timestamp': datetime.datetime(2009, 9, 14, 23, 23, 18),
+            'visible': True,
+            'version': 1,
+            'user': 'green525',
+            'tag': {
+                'admin_level': '9',
+                'boundary': 'administrative',
+            },
+            'nd': [
+                11949,
+                11950,
+                11951,
+                11952,
+                11953,
+                11954,
+                11955,
+                11956,
+                11957,
+                11958,
+                11959,
+                11960,
+                11961,
+                11962,
+                11963,
+                11964,
+                11949
+            ]
+        })
+
+    def test_WayGet_with_version(self):
+        self._conn_mock()
+
+        result = self.api.WayGet(4294967296, 2)
+
+        args, kwargs = self.api._conn.putrequest.call_args
+        self.assertEquals(args[0], 'GET')
+        self.assertEquals(args[1], '/api/0.6/way/4294967296/2')
+
+        self.assertEquals(result['id'], 4294967296)
+        self.assertEquals(result['changeset'], 41303)
+        self.assertEquals(result['user'], 'metaodi')
+
+    def test_WayGet_nodata(self):
+        self._conn_mock()
+
+        result = self.api.WayGet(321)
+
+        args, kwargs = self.api._conn.putrequest.call_args
+        self.assertEquals(args[0], 'GET')
+        self.assertEquals(args[1], '/api/0.6/way/321')
+
+        self.assertEquals(result, '')
+
+    def test_WayCreate(self):
+        self._conn_mock(auth=True)
+
+        # setup mock
+        self.api.ChangesetCreate = mock.Mock(
+            return_value=2222
+        )
+        self.api._CurrentChangesetId = 2222
+
+        test_way = {
+            'nd': [11949, 11950],
+            'tag': {
+                'highway': 'unclassified',
+                'name': 'Osmapi Street'
+            }
+        }
+
+        cs = self.api.ChangesetCreate({
+            'comment': 'This is a test way'
+        })
+        self.assertEquals(cs, 2222)
+
+        result = self.api.WayCreate(test_way)
+
+        args, kwargs = self.api._conn.putrequest.call_args
+        self.assertEquals(args[0], 'PUT')
+        self.assertEquals(args[1], '/api/0.6/way/create')
+
+        self.assertEquals(result['id'], 5454)
+        self.assertEquals(result['nd'], test_way['nd'])
+        self.assertEquals(result['tag'], test_way['tag'])
+
+    def test_WayUpdate(self):
+        self._conn_mock(auth=True)
+
+        # setup mock
+        self.api.ChangesetCreate = mock.Mock(
+            return_value=2222
+        )
+        self.api._CurrentChangesetId = 2222
+
+        test_way = {
+            'id': 876,
+            'nd': [11949, 11950],
+            'tag': {
+                'highway': 'unclassified',
+                'name': 'Osmapi Street Update'
+            }
+        }
+
+        cs = self.api.ChangesetCreate({
+            'comment': 'This is a test way'
+        })
+        self.assertEquals(cs, 2222)
+
+        result = self.api.WayUpdate(test_way)
+
+        args, kwargs = self.api._conn.putrequest.call_args
+        self.assertEquals(args[0], 'PUT')
+        self.assertEquals(args[1], '/api/0.6/way/876')
+
+        self.assertEquals(result['id'], 876)
+        self.assertEquals(result['version'], 7)
+        self.assertEquals(result['nd'], test_way['nd'])
+        self.assertEquals(result['tag'], test_way['tag'])
+
+    def test_WayDelete(self):
+        self._conn_mock(auth=True)
+
+        # setup mock
+        self.api.ChangesetCreate = mock.Mock(
+            return_value=2222
+        )
+        self.api._CurrentChangesetId = 2222
+
+        test_way = {
+            'id': 876
+        }
+
+        cs = self.api.ChangesetCreate({
+            'comment': 'This is a test way delete'
+        })
+        self.assertEquals(cs, 2222)
+
+        result = self.api.WayDelete(test_way)
+
+        args, kwargs = self.api._conn.putrequest.call_args
+        self.assertEquals(args[0], 'DELETE')
+        self.assertEquals(args[1], '/api/0.6/way/876')
+        self.assertEquals(result['id'], 876)
+        self.assertEquals(result['version'], 8)
+
+    def test_WayHistory(self):
+        self._conn_mock()
+
+        result = self.api.WayHistory(4294967296)
+
+        args, kwargs = self.api._conn.putrequest.call_args
+        self.assertEquals(args[0], 'GET')
+        self.assertEquals(args[1], '/api/0.6/way/4294967296/history')
+
+        self.assertEquals(len(result), 2)
+        self.assertEquals(result[1]['id'], 4294967296)
+        self.assertEquals(result[1]['version'], 1)
+        self.assertEquals(
+            result[1]['tag'], {
+                'highway': 'unclassified',
+                'name': 'Stansted Road',
+            }
+        )
+
+    def test_WayRelations(self):
+        self._conn_mock()
+
+        result = self.api.WayRelations(4295032193)
+
+        args, kwargs = self.api._conn.putrequest.call_args
+        self.assertEquals(args[0], 'GET')
+        self.assertEquals(args[1], '/api/0.6/way/4295032193/relations')
+
+        self.assertEquals(len(result), 1)
+        self.assertEquals(result[0]['id'], 4294968148)
+        self.assertEquals(result[0]['changeset'], 23123)
+        self.assertEquals(
+            result[0]['member'][4],
+            {
+                'role': '',
+                'ref': 4295032193,
+                'type': 'way',
+            }
+        )
+        self.assertEquals(
+            result[0]['tag'],
+            {
+                'type': 'fancy',
+            }
+        )
+
+    def test_WayFull(self):
+        self._conn_mock()
+
+        result = self.api.WayFull(321)
+
+        args, kwargs = self.api._conn.putrequest.call_args
+        self.assertEquals(args[0], 'GET')
+        self.assertEquals(args[1], '/api/0.6/way/321/full')
+
+        self.assertEquals(len(result), 17)
+        self.assertEquals(result[0]['data']['id'], 11949)
+        self.assertEquals(result[0]['data']['changeset'], 298)
+        self.assertEquals(result[0]['type'], 'node')
+        self.assertEquals(result[16]['data']['id'], 321)
+        self.assertEquals(result[16]['data']['changeset'], 298)
+        self.assertEquals(result[16]['type'], 'way')
+
+    def test_WaysGet(self):
+        self._conn_mock()
+
+        result = self.api.WaysGet([456, 678])
+
+        args, kwargs = self.api._conn.putrequest.call_args
+        self.assertEquals(args[0], 'GET')
+        self.assertEquals(args[1], '/api/0.6/ways?ways=456,678')
+
+        self.assertEquals(len(result), 2)
+        self.assertIs(type(result[456]), dict)
+        self.assertIs(type(result[678]), dict)
+        with self.assertRaises(KeyError):
+            self.assertIs(type(result[123]), dict)
diff --git a/tox.ini b/tox.ini
new file mode 100644
index 0000000..310e84e
--- /dev/null
+++ b/tox.ini
@@ -0,0 +1,11 @@
+[tox]
+envlist = py26,py27,py32,py33
+[testenv]
+commands=nosetests --verbose
+deps =
+    -r{toxinidir}/requirements.txt
+    -r{toxinidir}/test-requirements.txt
+[testenv:py26]
+deps =
+    unittest2
+    {[testenv]deps}

-- 
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/pkg-grass/python-osmapi.git



More information about the Pkg-grass-devel mailing list