I'm writing a Python library that represents some web API. Right now, my library directory looks close to this:
__init__.py
Account.py
Order.py
Category.py
requests.py
In __init__.py
, I have something like this:
from .Account import Account
from .Order import Order
from .Category import Category
from . import requests
This allows to use import cool_site
and then cool_site.Account(…)
and so on, but it has a following problem: when I play around with my code in IDLE, the object is then called cool_site.Account.Account
, which I feel is bad.
The next thing I don't feel great about is my code organization. Right now, my Account
class takes credentials on initialization, creates a requests.Session
object and then handles all communication with server, i.e. searching for orders and so on. This Account
class instance will then pass itself to all other instances, for example to Order
- so the order's instance will have .account
property holding the Account
instance which created it. When another class instance itself has to do something, for example change an order's comment (by calling o.comment = 'new comment'
, so by @comment.setter
decorator in the Order
class), it forwards that to an Account object which is passed to it on initialization, and then uses for example self.account.set_order_comment(new_comment)
. This method will then use all the web requests to achieve that goal.
The last thing I'd like to ask about is how and where to keep low-level request templates. Right now I have it in cool_site.requests
submodule, and there are different functions for different requests, for example SetOrderComment
for the case mentioned above (it's a function, so it should be lowercase, but in this case I think it resembles a class in a way - is that OK?). The Account.set_order_comment
will use it like this:
r = cool_site.requests.SetOrderComment(order.id, new_comment)
response = self._session.request(**r)
because this function returns a dict with arguments to Session.request
function from requests
library. The authentication headers are already set in the _session
property of Account
class instance. I feel it's a little bit ugly, but I don't have any better idea.
I'm sorry this question is so long and covers many aspects of API library design, but all the tips will be appreciated. In a way, all of the three questions above could be expressed as "How to do it better and cleaner?" or "How most of the Python developers do it?", or maybe even "What would feel most Pythonic?".
Throw at me any little tips you can think of.
Organize your modules into packages. Each package must contain a special __init__.py file. Your project should generally consist of one top-level package, usually containing sub-packages. That top-level package usually shares the name of your project, and exists as a directory in the root of your project's repository.
Modules and packages aren't just there to spread your Python code across multiple source files and directories—they allow you to organize your code to reflect the logical structure of what your program is trying to do.
I've been thinking about very similar things lately working on wistiapy
. Examples of my current thinking about client code organisation are in there. YMMV.
"One class per file" is more of a Java style guideline than Python. Python modules are a legitimate and important level in the code hierarchy, and you shouldn't worry about having more than one function or class in the same module. You could put all the model classes in a .models
module, and then from .models import (Account, Order, Category)
in __init__.py
.
More-or-less common practice for client libraries seems to be to have a client
module, containing something like a MyServiceClient
class. (eg the Segment client). This is where the networking logic goes. If you want to have the public interface be module-level functions, you can do some clever stuff with creating a default client and having the functions call methods on that.
Functions should be snake_case
, classes should be PascalCase
. Doing anything else tends to cause more confusion than benefit.
It seems like the big question you're dealing with is trying to choose between the "Active Record" pattern (some_account.set_order_comment(comment)
), and the "Data Mapper" pattern (set_order_comment(account, comment)
). Either will work and they each have their benefits and drawbacks. I've found the data mapper pattern -- using smart functions to manipulate fairly simple data classes -- simpler to begin with.
I find it helpful to design the public interface concurrently with something that uses that interface. In the calling code, you can write what you'd like to have to call, and then implement the client code "outside-in".
1) no upper case in names of .py file (also try to avoid _
)
so your files should be
__init__.py
account.py
order.py
category.py
requests.py
2) if you want to use like cool_site.Account
you need to add to __init__.py
from .account import Account
from .order import Order
from .category import Category
__all__ = [
'Account',
'Order',
'Category',
]
3) SetOrderComment
is bad name, use set_order_comment
4) If you write a python wrapper for communication with API, make method that do authorisation and other other stuff that is same in every API request. This method should take as params part of requests kwargs that are different for different API calls
for example
class API:
def __init__(self, endpoint:s str, api_key: str):
self.endpoint = endpoint
self.api_key = api_key
def _get_auth_headers(self) -> Dict[str, str]:
return {
'Authorization': 'Bearer ' + self.api_key,
}
def get(self, path, params)
resp = requester.get(
self.endpoint + path,
headers=self._get_auth_headers(),
params=params,
timeout=30,
)
self._check_api_response(resp)
payload = resp.json()
return payload
5) If you write a python API look at flask and django frameworks and projects with API written on them. You should find some good ides overthere.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With