Repository: ithaka/apiron

Issue:
Simpler import structure
**Is your feature request related to a problem? Please describe.**
It's tedious to import all the stuff you might need from `apiron`:

```python
from apiron.service.base import Service
from apiron.endpoint import Endpoint, JsonEndpoint
from apiron.client import ServiceCaller
```

**Describe the solution you'd like**
Given the fairly small footprint of the features of this package, the common needs should be available to import from the top-level of the package:

```python
from apiron import Endpoint, JsonEndpoint, Service, ServiceCaller
```

**Additional context**
Use `__all__` and `__init__.py` to make things available without moving them. Things to include are probably:

```
Endpoint
JsonEndpoint
StreamingEndpoint
StubEndpoint

Service
DiscoverableService

APIException
NoHostsAvailableException
UnfulfilledParameterException

ServiceCaller
Timeout
```

Discussion:

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2bc541d..3ffaafd 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
 and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 
 ## [Unreleased]
+### Changed
+- Simplify imports so that all commonly-used classes can be imported with `from apiron import <class>`
+
 ### Fixed
 - Error in calling a dynamic stub endpoint
 
diff --git a/README.md b/README.md
index 46c75b6..7cc25aa 100644
--- a/README.md
+++ b/README.md
@@ -26,8 +26,7 @@ A service definition requires a domain
 and one or more endpoints with which to interact:
 
 ```python
-from apiron.service.base import Service
-from apiron.endpoint import JsonEndpoint
+from apiron import JsonEndpoint, Service
 
 class GitHub(Service):
     domain = 'https://api.github.com'
diff --git a/apiron/__init__.py b/apiron/__init__.py
index e69de29..c42ec28 100644
--- a/apiron/__init__.py
+++ b/apiron/__init__.py
@@ -0,0 +1,4 @@
+from apiron.endpoint import *
+from apiron.service import *
+from apiron.client import ServiceCaller, Timeout
+from apiron.exceptions import APIException, NoHostsAvailableException, UnfulfilledParameterException
diff --git a/apiron/endpoint/__init__.py b/apiron/endpoint/__init__.py
index 6ade916..01d53b1 100644
--- a/apiron/endpoint/__init__.py
+++ b/apiron/endpoint/__init__.py
@@ -3,9 +3,10 @@ from apiron.endpoint.json import JsonEndpoint
 from apiron.endpoint.streaming import StreamingEndpoint
 from apiron.endpoint.stub import StubEndpoint
 
+
 __all__ = [
     'Endpoint',
     'JsonEndpoint',
     'StreamingEndpoint',
     'StubEndpoint',
-]
\ No newline at end of file
+]
diff --git a/apiron/service/__init__.py b/apiron/service/__init__.py
index e69de29..b119270 100644
--- a/apiron/service/__init__.py
+++ b/apiron/service/__init__.py
@@ -0,0 +1,8 @@
+from apiron.service.base import Service
+from apiron.service.discoverable import DiscoverableService
+
+
+__all__ = [
+    'Service',
+    'DiscoverableService',
+]
diff --git a/docs/advanced-usage.rst b/docs/advanced-usage.rst
index 0ca933a..d56ff28 100644
--- a/docs/advanced-usage.rst
+++ b/docs/advanced-usage.rst
@@ -14,8 +14,7 @@ Service and endpoints
 .. code-block:: python
 
     # test_service.py
-    from apiron.service.base import Service
-    from apiron.endpoint import Endpoint, JsonEndpoint, StreamingEndpoint
+    from apiron import Endpoint, JsonEndpoint, Service, StreamingEndpoint
 
     class HttpBin(Service):
         domain = 'https://httpbin.org'
@@ -36,7 +35,7 @@ Using all the features
 
     import requests
 
-    from apiron.client import Timeout
+    from apiron import Timeout
 
     from test_service import HttpBin
 
@@ -81,8 +80,7 @@ Here is an example where the resolver application always resolves to ``https://w
 
 .. code-block:: python
 
-    from apiron.client import ServiceCaller
-    from apiron.service.discoverable import DiscoverableService
+    from apiron import DiscoverableService
 
     class Eureka:
         @staticmethod
diff --git a/docs/getting-started.rst b/docs/getting-started.rst
index a2b7310..2b0c9f1 100644
--- a/docs/getting-started.rst
+++ b/docs/getting-started.rst
@@ -21,8 +21,7 @@ and one or more endpoints with which to interact:
 
 .. code-block:: python
 
-    from apiron.service.base import Service
-    from apiron.endpoint import JsonEndpoint
+    from apiron import JsonEndpoint, Service
 
     class GitHub(Service):
         domain = 'https://api.github.com'


Test patch:
diff --git a/tests/service/test_base.py b/tests/service/test_base.py
index 5b48222..dc3a052 100644
--- a/tests/service/test_base.py
+++ b/tests/service/test_base.py
@@ -1,6 +1,6 @@
 import pytest
 
-from apiron.service.base import Service
+from apiron import Service
 
 
 @pytest.fixture(scope='class')
diff --git a/tests/service/test_discoverable.py b/tests/service/test_discoverable.py
index bffb9b8..dc4afa0 100644
--- a/tests/service/test_discoverable.py
+++ b/tests/service/test_discoverable.py
@@ -1,6 +1,6 @@
 import pytest
 
-from apiron.service.discoverable import DiscoverableService
+from apiron import DiscoverableService
 
 
 class FakeResolver:
diff --git a/tests/test_client.py b/tests/test_client.py
index 56105d3..b294f28 100644
--- a/tests/test_client.py
+++ b/tests/test_client.py
@@ -2,8 +2,7 @@ from unittest import mock
 
 import pytest
 
-from apiron.client import ServiceCaller
-from apiron.exceptions import NoHostsAvailableException
+from apiron import ServiceCaller, NoHostsAvailableException
 
 class TestClient:
     @mock.patch('requests.sessions.Session', autospec=True)
diff --git a/tests/test_endpoint.py b/tests/test_endpoint.py
index f37cbaf..3d6ed06 100644
--- a/tests/test_endpoint.py
+++ b/tests/test_endpoint.py
@@ -4,85 +4,85 @@ from unittest import mock
 
 import pytest
 
-from apiron import endpoint
-from apiron.exceptions import UnfulfilledParameterException
-from apiron.service.base import Service
+import apiron
+
 
 @pytest.fixture
 def service():
-    class SomeService(Service):
+    class SomeService(apiron.Service):
         domain = 'http://foo.com'
     return SomeService
 
+
 class TestEndpoint:
     def test_call(self, service):
-        service.foo = endpoint.Endpoint()
+        service.foo = apiron.Endpoint()
         service.foo()
 
     def test_call_without_service_raises_exception(self):
-        foo = endpoint.Endpoint()
+        foo = apiron.Endpoint()
         with pytest.raises(TypeError):
             foo()
 
     def test_default_attributes_from_constructor(self):
-        foo = endpoint.Endpoint()
+        foo = apiron.Endpoint()
         assert '/' == foo.path
         assert 'GET' == foo.default_method
 
     def test_constructor_stores_passed_attributes(self):
-        foo = endpoint.Endpoint(path='/foo/', default_method='POST')
+        foo = apiron.Endpoint(path='/foo/', default_method='POST')
         assert '/foo/' == foo.path
         assert 'POST' == foo.default_method
 
     def test_format_response(self):
-        foo = endpoint.Endpoint()
+        foo = apiron.Endpoint()
         mock_response = mock.Mock()
         mock_response.text = 'foobar'
         assert 'foobar' == foo.format_response(mock_response)
 
     def test_required_headers(self):
-        foo = endpoint.Endpoint()
+        foo = apiron.Endpoint()
         assert {} == foo.required_headers
 
     def test_str_method(self):
-        foo = endpoint.Endpoint(path='/foo/bar/')
+        foo = apiron.Endpoint(path='/foo/bar/')
         assert '/foo/bar/' == str(foo)
 
     def test_format_path_with_correct_kwargs(self):
-        foo = endpoint.Endpoint(path='/{one}/{two}/')
+        foo = apiron.Endpoint(path='/{one}/{two}/')
         path_kwargs = {'one': 'foo', 'two': 'bar'}
         assert '/foo/bar/' == foo.get_formatted_path(**path_kwargs)
 
     def test_format_path_with_incorrect_kwargs(self):
-        foo = endpoint.Endpoint(path='/{one}/{two}/')
+        foo = apiron.Endpoint(path='/{one}/{two}/')
         path_kwargs = {'foo': 'bar'}
         with pytest.raises(KeyError):
             foo.get_formatted_path(**path_kwargs)
 
     def test_format_path_with_extra_kwargs(self):
-        foo = endpoint.Endpoint(path='/{one}/{two}/')
+        foo = apiron.Endpoint(path='/{one}/{two}/')
         path_kwargs = {'one': 'foo', 'two': 'bar', 'three': 'not used'}
         assert '/foo/bar/' == foo.get_formatted_path(**path_kwargs)
 
     def test_query_parameter_in_path_generates_warning(self):
         with warnings.catch_warnings(record=True) as warning_records:
             warnings.simplefilter('always')
-            foo = endpoint.Endpoint(path='/?foo=bar')
+            foo = apiron.Endpoint(path='/?foo=bar')
             assert 1 == len(warning_records)
             assert issubclass(warning_records[-1].category, UserWarning)
 
     def test_get_merged_params(self):
-        foo = endpoint.JsonEndpoint(default_params={'foo': 'bar'}, required_params={'baz'})
+        foo = apiron.JsonEndpoint(default_params={'foo': 'bar'}, required_params={'baz'})
         assert {'foo': 'bar', 'baz': 'qux'} == foo.get_merged_params({'baz': 'qux'})
 
     def test_get_merged_params_with_unsupplied_param(self):
-        foo = endpoint.JsonEndpoint(default_params={'foo': 'bar'}, required_params={'baz'})
+        foo = apiron.JsonEndpoint(default_params={'foo': 'bar'}, required_params={'baz'})
 
-        with pytest.raises(UnfulfilledParameterException):
+        with pytest.raises(apiron.UnfulfilledParameterException):
             foo.get_merged_params(None)
 
     def test_get_merged_params_with_empty_param(self):
-        foo = endpoint.JsonEndpoint(default_params={'foo': 'bar'}, required_params={'baz'})
+        foo = apiron.JsonEndpoint(default_params={'foo': 'bar'}, required_params={'baz'})
 
         with warnings.catch_warnings(record=True) as warning_records:
             warnings.simplefilter('always')
@@ -91,13 +91,13 @@ class TestEndpoint:
             assert issubclass(warning_records[-1].category, RuntimeWarning)
 
     def test_get_merged_params_with_required_and_default_param(self):
-        foo = endpoint.JsonEndpoint(default_params={'foo': 'bar'}, required_params={'foo'})
+        foo = apiron.JsonEndpoint(default_params={'foo': 'bar'}, required_params={'foo'})
         assert {'foo': 'bar'} == foo.get_merged_params(None)
 
 
 class TestJsonEndpoint:
     def test_format_response_when_unordered(self):
-        foo = endpoint.JsonEndpoint()
+        foo = apiron.JsonEndpoint()
         mock_response = mock.Mock()
 
         with mock.patch.object(mock_response, 'json') as mock_json:
@@ -106,7 +106,7 @@ class TestJsonEndpoint:
             mock_json.assert_called_once_with(object_pairs_hook=None)
 
     def test_format_response_when_ordered(self):
-        foo = endpoint.JsonEndpoint(preserve_order=True)
+        foo = apiron.JsonEndpoint(preserve_order=True)
         mock_response = mock.Mock()
 
         with mock.patch.object(mock_response, 'json') as mock_json:
@@ -115,13 +115,13 @@ class TestJsonEndpoint:
             mock_json.assert_called_once_with(object_pairs_hook=collections.OrderedDict)
 
     def test_required_headers(self):
-        foo = endpoint.JsonEndpoint()
+        foo = apiron.JsonEndpoint()
         assert {'Accept': 'application/json'} == foo.required_headers
 
 
 class TestStreamingEndpoint:
     def test_format_response(self):
-        foo = endpoint.StreamingEndpoint()
+        foo = apiron.StreamingEndpoint()
         mock_response = mock.Mock()
         assert mock_response.iter_content(chunk_size=None) == foo.format_response(mock_response)
 
@@ -132,14 +132,14 @@ class TestStubEndpoint:
         """
         Test initializing a stub endpoint with a stub response
         """
-        stub_endpoint = endpoint.StubEndpoint(stub_response='stub response')
+        stub_endpoint = apiron.StubEndpoint(stub_response='stub response')
         assert 'stub response' == stub_endpoint.stub_response
 
     def test_extra_params(self):
         """
         Test initializing a stub endpoint with extra params
         """
-        stub_endpoint = endpoint.StubEndpoint(
+        stub_endpoint = apiron.StubEndpoint(
             stub_response='stub response',
             path='/some/path/',
             default_params={'param_name': 'param_val'},
@@ -156,7 +156,7 @@ class TestStubEndpoint:
         """
         Test calling a ``StubEndpoint`` with a static response
         """
-        service.stub_endpoint = endpoint.StubEndpoint(
+        service.stub_endpoint = apiron.StubEndpoint(
             stub_response='stub response',
         )
         actual_response = service.stub_endpoint()
@@ -174,7 +174,7 @@ class TestStubEndpoint:
                 else:
                     return {'default': 'response'}
 
-            service.stub_endpoint = endpoint.StubEndpoint(stub_response=stub_response)
+            service.stub_endpoint = apiron.StubEndpoint(stub_response=stub_response)
             actual_response = service.stub_endpoint(**call_kwargs)
             assert actual_response == expected_response