-
-
Notifications
You must be signed in to change notification settings - Fork 839
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Context-managed client usage #422
Comments
@tomchristie I understand HostedAPI is going to help answer the questions you mentioned in your comment? Here's my take on what a Flask setup would look like, though. Long-lived clientI don't think this is possible with Flask — it doesn't seem to support app-wide teardown callbacks, but only per-request callbacks when the app context is torn down. I'd argue a single long-lived client would lead to all sorts of issues related to network resources over time though, so not sure this is something users should even consider. Per-request clientWe could naively # project/tmdb.py
import os
import typing
import httpx
from flask import Flask, g
TMDB_API_KEY = os.environ["TMDB_API_KEY"]
def get_tmdb() -> httpx.Client:
if "tmdb" not in g:
# Create a client.
# Note that this hints at keeping `Client.__enter__()` in
# its current state - not doing anything particular.
# (This is similar to the `open()` built-in not requiring us to manually call `__enter__()`
# to use it without the `with` statement.)
g.tmdb = httpx.Client(
base_url="https://api.themoviedb.org/3", headers={"x-api-key": TMDB_API_KEY}
)
return g.tmdb
def close_tmdb() -> None:
# Ensure the client is closed if `get_tmdb()` was called.
tmdb: httpx.Client = g.pop("tmdb", None)
if tmdb is not None:
tmdb.close()
def init_app(app: Flask) -> None:
app.teardown_appcontext(close_tmdb)
# project/movies.py
from flask import Blueprint, jsonify, request
from .tmdb import get_tmdb
bp = Blueprint("movies", __name__, url_prefix="movies")
@bp.route("/search")
def search_movies():
q = request.args.get("q")
with get_tmdb() as tmdb:
# If we made multiple requests to the TMDb API here,
# and assuming TMDb supports HTTP/2,
# then we'd be reusing the connection across requests.
# Would it be desirable to share connections across views, anyway?
r = tmdb.get("/search/tv", params={"query": q, "page": 1})
r.raise_for_status()
rows = r.json()
shows = [{"id": row["id"], "title": row["name"]} for row in rows]
return jsonify(shows)
# project/app.py
from flask import Flask
def create_app() -> Flask:
from . import tmdb
from . import movies
app = Flask(__name__)
tmdb.init_app(app)
app.register_blueprint(movies.bp)
return app
if __name__ == "__main__":
app = create_app()
app.run() Scaling upIn bigger projects, users would probably want to factor API calls into some kind of service class acting as a wrapper around the # project/tmdb.py
import os
import typing
import httpx
from flask import Flask, g
TMDB_API_KEY = os.environ["TMDB_API_KEY"]
class TMDbService:
def __init__(self, client: httpx.Client):
self.client = client
def search_movies(self, q: str) -> typing.List[dict]:
r = self.client.get("/search/tv", params={"query": q, "page": 1})
r.raise_for_status()
rows = r.json()
shows = [{"id": row["id"], "title": row["name"]} for row in rows]
return shows
def close(self) -> None:
self.client.close()
def get_tmdb() -> TMDbService:
if "tmdb" not in g:
client = httpx.Client(
base_url="https://api.themoviedb.org/3", headers={"x-api-key": TMDB_API_KEY}
)
g.tmdb = TMDbService(client=client)
return g.tmdb
def close_tmdb(exc: typing.Optional[Exception]) -> None:
tmdb: TMDbService = g.pop("tmdb", None)
if tmdb is not None:
tmdb.close()
def init_app(app: Flask) -> None:
app.teardown_appcontext(close_tmdb)
# project/movies.py
from flask import Blueprint, jsonify, request
from .tmdb import get_tmdb
bp = Blueprint("movies", __name__, url_prefix="movies")
@bp.route("/search")
def search_movies():
q = request.args.get("q")
with get_tmdb() as tmdb:
return jsonify(tmdb.search_movies(q=q)) ConclusionI think my take-away from this is that in a WSGI web app context, context-managed usage with the In fact, it's clearer to me now that enforcing context-managed usage is not synonymous for enforcing |
You do want to share the connection pool across all outgoing requests, so a single Client instance is preferable. I think my clear take home is that "yeah we want to support with-block-context-managed usage, but we don't want to hard enforce it, and should have an explicit I don't think there's a significant enough difference between the HTTP/1.1 and HTTP/2 cases for us to say that we only support HTTP/2 in a with-block:
Wrt. "structured concurrency" what that'd map to is "Prefer structured concurrency as the norm everywhere, but do allow for explicitly breaking out from that in some limited well-defined contexts." It's actually totally OK to have a dangling thread of control that's just "send pings on any open connections, and time out after a determined period". It's just that should be an explicitly non-standard case vs. a standard construct for "please perform these two seperate bits of application logic concurrently". |
This thread python-trio/hip#125 from @njsmith is nicely timed. Pretty much in line with how I'd see it. |
That thread couldn't be any better on the timing. I like the global pool with a system level watcher. :) |
To be clear, this is the way I now see it as well after the quick Flask experiment above. :) And that thread on urllib3 seems in line with that reasoning. |
Since it seems we’ve reached a common understanding, are there any actions points to take away from this issue before closing? Documentation updates maybe? |
We ought to:
We could(?) consider switching our top-level API cases to use a lazily created singleton client instance, so that they get connection pooling, and HTTP/2 support. We'd need to switch off cookie persistence on the singleton client, which isn't currently something we support doing, so we'd want a preliminary issue for supporting something like |
I thought you had an ABC for cookie storage? Just make a |
Or maybe I'm misremembering, but if you don't you probably should :-) |
We have a cookiejar abstraction, I think setting it to an always-empty jar like you describe is best. :) |
#389 has seen some interesting discussions on context-manager usage of
Client
andAsyncClient
, esp. #389 (comment) and #389 (comment). Figured it would be worth porting the discussion to a dedicated issue.Retransmission of the two comments mentioned above:
@tomchristie
@sethmlarson
Let's continue discussion here?
The text was updated successfully, but these errors were encountered: