We have a bunch of api's with different versions in urls.py, eg
api/v1
api/v2
api/v3
. We want to implement swagger with drf-spectacular, but we only want to expose api/v3 endpoints.
Is there a way to do this? I cannot make sense of the documentation.
Thanks
This works for me:
def preprocessing_filter_spec(endpoints):
filtered = []
for (path, path_regex, method, callback) in endpoints:
# Remove all but DRF API endpoints
if path.startswith("/api/"):
filtered.append((path, path_regex, method, callback))
return filtered
In settings:
"PREPROCESSING_HOOKS": ["common.openapi.preprocessing_filter_spec"],
Worked it out. Copied the custom preprocessing hook function, changed the pass statement to filter what I did not require in the endpoints, then mapped the location of the file and function in my spectacular settings for preprocessed hooks.
Related
I am new to Django and I have 2 endpoints in a REST API:
api.myapp.com/PUBLIC/
api.myapp.com/PRIVATE/
As suggested by the names, the /PUBLIC endpoint is open to anyone. However, I want the /PRIVATE endpoint to only accept calls from myapp.com (my frontend). How can I do this? Thanks!
Without knowing how you apps and servers are setup, I think you can solve this problem with the django-cors-headers package. I skimmed the entire read me, and the signals at the bottom looks like a solution to your problem. This part here:
A common use case for the signal is to allow all origins to access a
subset of URL's, whilst allowing a normal set of origins to access all
URL's. This isn't possible using just the normal configuration, but it
can be achieved with a signal handler.
First set CORS_ALLOWED_ORIGINS to the list of trusted origins that are
allowed to access every URL, and then add a handler to
check_request_enabled to allow CORS regardless of the origin for the
unrestricted URL's. For example:
# myapp/handlers.py from corsheaders.signals import check_request_enabled
def cors_allow_api_to_everyone(sender, request, **kwargs):
return request.path.startswith('/PUBLIC/')
check_request_enabled.connect(cors_allow_api_to_everyone)
So for most cases django-cors-headers are set as an option for the entire project, but there seems to be a way here for you to allow a subset of your api (/PUBLIC in your case) to be allowed for everyone, but the rest is private.
So your config would be
CORS_ALLOWED_ORIGINS = [
"https://myapp.com",
]
That allows myapp.com to reach everything.
cors_allow_api_to_everyone is a function checking for a truth value.
If it is true, the request is allowed.
check_request_enabled.connect(cors_allow_api_to_everyone) connects your truth-check-function to the django-cors-headers signal.
I'm currently developing an ckan extension, where i need to redirect to a url on a different domain.
In my plugin i defined a custom action function:
#side_effect_free
def download_json(context, data_dict):
toolkit.redirect_to('http://my.json-builder.com?id=1234')
But when i call this endpoint i just get following response:
response screenshot
So i assume that the action function is called, but the redirect_to call does not redirect to the url i defined.
Thanks for your help!
Florian
It's a bit hard to figure out what you're trying to accomplish but here's a few things I hope will help.
Short Answer:
No, you can't redirect from an API endpoint in CKAN. The endpoint response in CKAN is built up and expects certain things from your action. Your action should return some kind of result. In your case it's returning nothing but trying to redirect. A logic action function with IActions is not the same as a Blueprint or pylons controller action.
See Making an API request docs, specifically the breakdown of an API response in CKAN. Also, you can review the pylons implementation that builds up the API response or the flask blueprints implementation.
More Info to help with your approach:
You say you are trying to call an endpoint that redirects a user to a different domain url. Based on this consider the following:
The first thing I thought you wanted was to have a url that someone goes to through the web interface of your site and are redirected to another site. In this case your example code of toolkit.redirect_to('http://my.json-builder.com?id=1234') makes sense and works for a custom controller action using/implemented with IRoutes or if you're using flask then IBlueprint. A User would go to a URL on your site such as http://localhost.com/download_json and be redirected to the new URL/site in their browser.
If you are intending this to be an API call for other users this starts to feel a little bit odd. If a user is using your API, they would expect to get results from your site in JSON CKAN's API is designed to return JSON. Someone consuming your API endpoint would not expect to be redirected to another site e.g. if I called http://localhost.com/api/3/action/download_json I would expect to get a JSON object like
{
help: "http://localhost/api/3/action/help_show?name=download_json",
success: true,
result: {
...
}
}
They would look for success to make sure the call worked and then they would use the result to keep moving forward with their desired processes. If you do want someone via an API to get redirect info I'd likely return the redirect url as the result e.g. result: {'redirect_url': 'http://my.json-builder.com?id=1234'} and document this well in your extension's API docs (e.g. why you're returning this endpoint, what you expect someone to do with it, etc).
If this is an API call for your own extension I'm guessing what you are trying to do is use my.json-builder.com to build a json of something (a dataset maybe?) and return that json as the result at your endpoint or maybe even consume the result to make something else? If that's the case, then in your function you could make the call to my.json-builder.com, process the results and return the results to the user. In this case, you're not actually wanting to redirect a user to a new site but instead make a call to the new site to get some results. If you actually want the results for your extension you don't need an additional endpoint. You could make the call from your extension, consume the results and return the desired object you're trying to create.
Hope this helps and sorry if I've miss-understood completely.
I'm struggling to find the proper way to setup my flask routes when moving my app to being hosted in a subdirectory. I've read a bunch of great answers on this subject, most notably this answer on adding a prefix to all routes. However, I think my situation is slightly different. I want to ONLY prefix the URLs I generate with url_for, not respond to those URLs. Is there a way to do this?
For example, assume my flask app is hosted at http://www.acme.com/tools/. The main acme.com domain is setup with a proxy pass to pass all requests from /tools/* to my app. So, my app only sees requests like /, /product_1/ even though the full URL is http://www.acme.com/tools/product_/, etc. So, my app.route rules can use / as the base route. The problem is I use url_for everywhere and I want the urls generated to be the full url like /tools/product_1, not /product_1. Is there a way to achieve this?
I've tried using blueprints but those will make the #app.route and url_for respond to /tools/xyz. Is there a more simple solution to this issue or is this a non-standard way to handle this issue?
I would take a look at this snippet: http://flask.pocoo.org/snippets/35/
I'm not sure I love the idea of modifying your WSGI environment as the only solution to this problem. You can always wrap url_for. (note that this won't work for _external=True)
def my_url_for(*args, **kwargs):
return '/tools' + url_for(*args, **kwargs)
Be sure to set your APPLICATION_ROOT to /tools so that cookies are only accessible from the app.
I found a different way to handle this without having to provide a wrapper for url_for. I'm using a custom routing rule to append the prefix. This means none of the app.route decorator calls have to be changed and all calls to url_for will automatically get the prefix added. Are there any caveats I'm missing by overriding the url_rule_class?
Add the following to your __init__.py after setting up your app.
from werkzeug.routing import Rule
class PrefixRule(Rule):
def build(self, *args, **kwargs):
domain_part, url = super(PrefixRule, self).build(*args, **kwargs)
return domain_part, u'%s%s' % (app.config['PREFIX'], url)
app.url_rule_class = PrefixRule
I'm using Django Rest Framework. In a permissions class I check the value of two custom http headers: mt_api_token and mt_api_key.
In tests I call the API with my custom headers. sign_request() returns {'mt_api_key': api_client.client_id, 'mt_api_token': token}.
class APIv1SimpleTestCase(APITestCase):
[...]
response = self.client.get(url, format='json', **sign_request("", api_client=oauth2_client))
This is a legacy system and headers must have underscores instead of dashes.
The permissions class:
class AuthPermissions(permissions.BasePermission):
def is_token_valid(self, request):
mt_api_token = request.META['mt_api_token']
[...]
Tests pass.
However, if I call the api using httpie:
$ http :8000/api/v1/endpoint1/ mt_api_key:0a9..66 mt_api_token:7b2...8
I get keyError exception because request.META['mt_api_key'] and request.META['mt_api_token'] don't exist.
I have to use request.META['HTTP_MT_API_TOKEN'], which makes sense according to DRF documentation for request.META, but I can't find out why tests pass. if I change the implementation to request.META['HTTP_...'] my tests fail.
This is working fine in the tests, because the tests are manually setting the request.META keys to match what you are passing in, mt_api_token and mt_api_key. You aren't seeing any errors because this is perfectly valid, but it doesn't match what you are expecting. This is covered very briefly in the Django advanced testing documentation, but essentially the extra arguments are directly added to the META dictionary.
When your request goes through the WSGI handler provided by Django, any HTTP headers are automatically prefixed with HTTP_ per the WSGI specification. This applies to most client-provided HTTP headers, including the Authorization header which is typically used to authorize users on an API (using Basic authentication, OAuth, etc.), which is put into the request.META dictionary as HTTP_AUTHORIZATION. Note that the keys are also always uppercase, as HTTP header names are case-insensitive.
You can fix your tests by having your sign_request method return the keys as HTTP_MT_API_TOKEN and HTTP_MT_API_KEY, which are what they would be passed in as when coming through a browser. You will also need to adjust your view code to reference those new keys.
I see 'Sorry, not implemented yet. Please append "?format=json" to
your URL.'. I need always append string "?format=json". Can I make a
output in JSON by default?
Regards,
Vitaliy
From the tastypie cookbook, in order to change the default format, you need to override the determine_format() method on your ModelResource:
class MyResource(ModelResource):
....
def determine_format(self, request):
return 'application/json'
The above link demonstrates alternative methods of determining output format.
Also, I don't think a valid answer is essentially "You don't need this".
Edit
It appears GregM's answer is probably (I haven't tested it) the most correct with the new version of TastyPie, as per documentation putting the following in your settings.py will restrict the serialization output to json.
TASTYPIE_DEFAULT_FORMATS = ['json']
As of tastypie 0.9.13, if you do not need XML support you can disable it globally by setting TASTYPIE_DEFAULT_FORMATS to just ['json'] in your settings.py file. Requests should then default to JSON.
I've tested setting TASTYPIE_DEFAULT_FORMATS to ['json'] but it doesn't prevent the "Sorry not implemented yet" message when viewing the API from a browser.
I am able to make that warning go away by forcing the "Accept" header to 'application/json' in a middleware:
class TastyJSONMiddleware(object):
"""
A Django middleware to make the Tastypie API always output in JSON format
instead of telling browsers that they haven't yet implemented text/html or
whatever.
WARNING: This includes a hardcoded url path for /api/. This is not 'DRY'
because it means you have to edit two places if you ever move your API
path.
"""
api_prefix = '/api/'
def process_request(self, request):
if request.path.startswith(self.api_prefix):
request.META['HTTP_ACCEPT'] = 'application/json'
Tasytpie has the defaults set as 'application/json'. But that is overridden by Browser request.
According to Tastypie, the default is overridden by Request Header ACCEPT and your format specification in GET ie. ?format=json. When you send request from browsers, if you see the Request HTTP Header sent, its something like -
Accept:text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
The application/xml overrides the default in Tastypie Resource. Therefore, either you can set Browser Header to have 'application/json' (Bad idea) or you just specify in GET.
If you hit the same API url using CURL, you will see the JSON output without specifying that in GET.
To examine/test your REST API, use a Rest client instead of a browser, preferably one that knows how to pretty print JSON. I use the Postman plugin for Google Chrome.
If you want pretty json in command line:
curl https://api.twitter.com/1.1/search/tweets.json | python -m json.tool