How to get defined route paths in FastAPI? - python

I have my FastAPI app define in server.py
app = FastAPI(
debug=True, title="Microservice for APIs",
description="REST APIs",
version="0.0.1",
openapi_url="/v3/api-docs",
middleware=[
Middleware(AuthorizationMiddleware, authorizor=Auth())
])
In __init__.py, I have routes defined
from fastapi import APIRouter
api_router = APIRouter()
api_router.include_router(impl_controller.router, prefix="/impl/data",
tags=["APIs for Impl Management"])
In impl_controller.py, I have define routes like this
#router.get('{id}/get_all')
def hello_world():
return {"msg": "Hello World"}
#router.get('{id}/get_last')
def test():
return {"msg": "test"}
In the middleware, I'm trying to get request route and not the URL
def check_for_api_access(self, request: Request):
request_path = request.scope['path']
# route_path = request.scope['endpoint'] # This does not exists
url_list = [
{'path': route.path, 'name': route.name}
for route in request.app.routes
]
Result I'm expecting is: {id}/get_all for 1st request and {id}/get_last for 2nd request.
I'm able to get list of all paths in url_list but I want route path for the specific request
Tried solution provided here: https://github.com/tiangolo/fastapi/issues/486 that also not working for me

Try this:
def check_for_api_access(self, request: Request):
api_route = next(item for item in request.app.routes if isinstance(item, APIRoute) and item.dependant.cache_key[0] == request.scope['endpoint'])
print(api_route.path)

You may not be able to accomplish exactly what you need, though you can get very close with the standard framework (i.e. no fancy alterations of the framework).
In the middleware, you can access the request directly. This way, you'll be able to inspect the requested url, as documented in https://fastapi.tiangolo.com/advanced/using-request-directly/?h=request . The attributes that are accessible are described here https://www.starlette.io/requests/
N.B. Since you posted just snippets, it's very difficult to say where values/variables come from.
In your case, it's dead simple what is not working. If you looked at the urls I posted, the starlette docs, shows the attributes that you can access from a request. This contains exactly the attribute you are looking for.
Basically, change request_path = request.scope['path'] into request_path = request.url.path. If you have a prefix, then you'll also get that and that's why I said You may not be able to accomplish exactly what you need, though you can get very close with the standard framework (i.e. no fancy alterations of the framework). Still, if you know your prefix, you can remove it from the path (it's just a string).

Related

FastAPI APIRouter prefix are showing twice. Why?

notecard_router = APIRouter(
prefix = "/notecard",
tags = ['notecard']
)
Hi, I am trying to include a router to my app, but for some reason, the prefix that I give in the API router comes twice in the API paths. I have no idea why this is happening.
This is happening because from what I'm able to tell, you're not structuring your endpoints the way you want them. Instead of creating the API router and adding your prefix to it in each endpoint,
from fastapi import APIRouter
app = APIRouter(tags=["notecard"], prefix="/notecard")
#app.get("/notecard/index")
async def root():
...
When you do something like this, your endpoint actually becomes /prefix/notecard/index, which is /notecard/notecard/index. It looks like that's what you're doing. Instead of that, you can do
from fastapi import APIRouter
app = APIRouter(tags=["notecard"], prefix="/notecard")
#app.get("/index")
async def root():
...
And FastAPI will handle the prefixing for you.
Also, relevant snippets of your code or anything that can be used to reproduce your issue would make answering easier :)

Unit testing Python Google Cloud HTTP functions - need to pass in a request object

I'm getting started writing Google Cloud http Functions using Python and I would like to include unit tests. My function only responds to POST requests so the basic outline of my function is:
def entry_point(request):
if request.method == 'POST':
# do something
else:
# throw error or similar
I want to write a simple unit test to ensure that if the function receives a GET request the response has a 405 status.
Hence in my unit test I need to pass in a value for the request parameter:
def test_call(self):
req = #need a way of constructing a request
response = entry_point(req)
assert response.status == 405 # or something like this
Basically I need to construct a request then check that the response status is what I expect it to be. I've googled around and found loads of pages that talk about mocking and all sorts of stuff that I frankly don't understand (I'm not a crash hot developer) so I'm hoping someone can help me with an idiot's guide of doing what I need to do.
I did find this: https://cloud.google.com/functions/docs/bestpractices/testing#unit_tests_2:
from unittest.mock import Mock
import main
def test_print_name():
name = 'test'
data = {'name': name}
req = Mock(get_json=Mock(return_value=data), args=data)
# Call tested function
assert main.hello_http(req) == 'Hello {}!'.format(name)
def test_print_hello_world():
data = {}
req = Mock(get_json=Mock(return_value=data), args=data)
# Call tested function
assert main.hello_http(req) == 'Hello World!'
which kinda helped but it doesn't explain how I can specify the request method (i.e. GET, POST etc...).
It is probably late to also comment on this one, but I was going through the same quest. How to REALLY unit test a cloud function without mocks and/or Flask test client. For the records, at the Google Cloud Platform Python Docs Samples, it is explained how to use Flask test_request_context() functionality with some examples to achieve this (without having to create Request object by hand).
Just dawned on me that I was rather over-thinking this somewhat. For the purposes of testing my code doesn't actually require a real Flask request passed to it, it merely requires an object that has the attributes my code refers to. For me, this will do:
import unittest
from main import entry_point
class Request:
def __init__(self, method):
self.method = method
class MyTestClass(unittest.TestCase):
def test_call_with_request(self):
request = Request("GET")
response = entry_point(request)
assert response.status_code == 405

Testing Flask routes do and don't exist

I'm creating a large number of Flask routes using regular expressions. I'd like to have a unit test that checks that the correct routes exist and that incorrect routes 404.
One way of doing this would be to spin up a local server and use urllib2.urlopen or the like. However, I'd like to be able to run this test on Travis, and I'm assuming that's not an option.
Is there another way for me to test routes on my application?
Use the Flask.test_client() object in your unittests. The method returns a FlaskClient instance (a werkzeug.test.TestClient subclass), making it trivial to test routes.
The result of a call to the TestClient is a Response object, to see if it as 200 or 404 response test the Response.status_code attribute:
with app.test_client() as c:
response = c.get('/some/path/that/exists')
self.assertEquals(response.status_code, 200)
or
with app.test_client() as c:
response = c.get('/some/path/that/doesnt/exist')
self.assertEquals(response.status_code, 404)
See the Testing Flask Applications chapter of the Flask documentation.
Martjin's answer surely solve your issue, but some times you don't have the time (or will) to mock all the components you call in a route you want to test for existence.
And why would you need to mock? Well, the call get('some_route') will first check for this route to exists and then ... it will be executed!
If the view is a complex one and you need to have fixtures, envs variables and any other preparation just for test if the route exists, then you need to think again about your test design.
How to avoid this:
You can list all the routes in your app. An check the one you're testing is in the list.
In the following example, you can see this in practice with the implementation of a site-map.
from flask import Flask, url_for
app = Flask(__name__)
def has_no_empty_params(rule):
defaults = rule.defaults if rule.defaults is not None else ()
arguments = rule.arguments if rule.arguments is not None else ()
return len(defaults) >= len(arguments)
#app.route("/site-map")
def site_map():
links = []
for rule in app.url_map.iter_rules():
# Filter out rules we can't navigate to in a browser
# and rules that require parameters
if "GET" in rule.methods and has_no_empty_params(rule):
url = url_for(rule.endpoint, **(rule.defaults or {}))
links.append((url, rule.endpoint))
# links is now a list of url, endpoint tuples
references:
get a list of all routes defined in the app
Helper to list routes (like Rail's rake routes)
Another way of testing a URL without executing the attached view function is using the method match of MapAdapter.
from flask import Flask
app = Flask(__name__)
#app.route('/users')
def list_users():
return ''
#app.route('/users/<int:id>')
def get_user(id):
return ''
Testing
# Get a new MapAdapter instance. For testing purpose, an empty string is fine
# for the server name.
adapter = app.url_map.bind('')
# This raise werkzeug.exceptions.NotFound.
adapter.match('/unknown')
# This raises werkzeug.exceptions.MethodNotAllowed,
# Although the route exists, the POST method was not defined.
adapter.match('/users', method='POST')
# No exception occurs when there is a match..
adapter.match('/users')
adapter.match('/users/1')
From Werkzeug documentation:
you get a tuple in the form (endpoint, arguments) if there is a match.
Which may be useful in certain testing scenarios.

bottlepy route match any url with exceptions

I'm writing a template on bottle for every request. So localhost and locahost/mypage and localhost/mypage/about will call the same template. I checked here and find some good examples on matching all urls like this:
from bottle import route, run, template, static_file
#route("/<url:re:.+>")
def hello(url):
return template('page_template', url=url)
#route('/static/<filepath:path>', name='static')
def server_static(filepath):
return static_file(filepath, root='static')
run()
My questions are:
1) It doesn't match root. So if I type "localhost", it doesn't work.
2) Since there are static files, I have another route for static files serving. So if I type localhost/static/page, it doesn't return "hello world".
I believe I need to modify the regex (/<:re:.+>) to deal with both situations. Any help would be greatly appreciated,
#Michael
Good news: You can "stack" routes quite straightforwardly. Just do this:
#route("/")
#route("/<url:re:.+>")
def hello(url):
return template('page_template', url=url)
That will treat root the same as your existing regex route.
As for overlapping routes, according to the documentation, dynamic routes are evaluated in the order in which they were defined.

Block requests from *.appspot.com and force custom domain in Google App Engine

How can I prevent a user from accessing my app at example.appspot.com and force them to access it at example.com? I already have example.com working, but I don't want users to be able to access the appspot domain. I'm using python.
You can check if os.environ['HTTP_HOST'].endswith('.appspot.com') -- if so, then you're serving from something.appspot.com and can send a redirect, or otherwise alter your behavior as desired.
You could deploy this check-and-redirect-if-needed (or other behavior alteration of your choice) in any of various ways (decorators, WSGI middleware, inheritance from an intermediate base class of yours that subclasses webapp.RequestHandler [[or whatever other base handler class you're currently using]] and method names different than get and post in your application-level handler classes, and others yet) but I think that the key idea here is that os.environ is set by the app engine framework according to CGI standards and so you can rely on those standards (similarly WSGI builds its own environment based on the values it picks up from os.environ).
The code posted above has two problems - it tries to redirect secure traffic (which isn't supported on custom domains), and also your cron jobs will fail when Google call them on your appspot domain and you serve up a 301.
I posted a slightly modified version to my blog:
http://blog.dantup.com/2009/12/redirecting-requests-from-appid-appspot-com-to-a-custom-domain
I've included the code below for convenience.
from google.appengine.ext import webapp
from google.appengine.ext.webapp.util import run_wsgi_app
def run_app(url_mapping):
application = webapp.WSGIApplication(url_mapping, debug=True)
application = redirect_from_appspot(application)
run_wsgi_app(application)
def redirect_from_appspot(wsgi_app):
"""Handle redirect to my domain if called from appspot (and not SSL)"""
from_server = "dantup-blog.appspot.com"
to_server = "blog.dantup.com"
def redirect_if_needed(env, start_response):
# If we're calling on the appspot address, and we're not SSL (SSL only works on appspot)
if env["HTTP_HOST"].endswith(from_server) and env["HTTPS"] == "off":
# Parse the URL
import webob, urlparse
request = webob.Request(env)
scheme, netloc, path, query, fragment = urlparse.urlsplit(request.url)
url = urlparse.urlunsplit([scheme, to_server, path, query, fragment])
# Exclude /admin calls, since they're used by Cron, TaskQueues and will fail if they return a redirect
if not path.startswith('/admin'):
# Send redirect
start_response("301 Moved Permanently", [("Location", url)])
return ["301 Moved Peramanently", "Click Here %s" % url]
# Else, we return normally
return wsgi_app(env, start_response)
return redirect_if_needed
def redirect_from_appspot(wsgi_app):
def redirect_if_needed(env, start_response):
if env["HTTP_HOST"].startswith('my_app_name.appspot.com'):
import webob, urlparse
request = webob.Request(env)
scheme, netloc, path, query, fragment = urlparse.urlsplit(request.url)
url = urlparse.urlunsplit([scheme, 'www.my_domain.com', path, query, fragment])
start_response('301 Moved Permanently', [('Location', url)])
return ["301 Moved Peramanently",
"Click Here" % url]
else:
return wsgi_app(env, start_response)
return redirect_if_needed
You can redirect it to your custom domain (here example.com) by overriding the initialize method in your webapp2.RequestHandler base class the following way :
def initialize(self, *a, **kw):
webapp2.RequestHandler.initialize(self, *a, **kw)
if self.request.host.endswith('appspot.com'):
self.request.host = 'example.com'
self.redirect(self.request.url, permanent=True)
In case you are using webapp2, a slightly easier (depending on your use case) solution is possible using its DomainRoute.
If this is your URL map:
url_map = [
('/', HomeHandler),
('/about', AboutHandler),
('/admin/(.+)', AdminHandler),
MailForwardHandler.mapping(),
]
application = webapp2.WSGIApplication(url_map, debug=True)
and the admin and mail should not redirect, then add the redirect routes as follows:
from webapp2_extras import routes
url_map = [
routes.DomainRoute('appid.appspot.com', [
routes.RedirectRoute('/', redirect_to='http://app.com', schemes=['http']),
routes.RedirectRoute('/about', redirect_to='http://app.com/about', schemes=['http']),
],
('/', HomeHandler),
('/about', AboutHandler),
('/admin/(.+)', AdminHandler),
MailForwardHandler.mapping(),
]
application = webapp2.WSGIApplication(url_map, debug=True)
This will, as specified, only redirect http, not https. It uses a permanent redirect (301), as desired.
You can also pass a function to redirect_to, as described in the documentation, which should make it easier when you have lots of rules.

Categories

Resources