Progress towards working /judgements/ path#4
Conversation
| def configure(bp, config, trusted_clients, centralauth, state, stream): | ||
|
|
||
| def configure(bp, trusted_clients, source): | ||
| @bp.route("/v1/judgements/<string:context>", methods=["get"]) |
There was a problem hiding this comment.
Swagger might be the best place to experiment with routes...
|
|
||
| from ... import responses | ||
|
|
||
| def configure(bp, config, trusted_clients, centralauth, state, stream): |
There was a problem hiding this comment.
Nesting these route functions under a top-level function rather than a class will make testing difficult, won't it? Aren't these inner functions inaccessible?
There was a problem hiding this comment.
They are inaccessible at first, but they become part of bp. This actually provides a nice framework for testing because the bp object is a general routing abstraction. Rather than using global variables, shared resources are enclosed in the configure() function. This allows us to, for example, safely pass an arbitrary configuration in testing or when constructing the routes to use. We can also easily include mocks for trusted_clients, centralauth, etc.
There was a problem hiding this comment.
Okay, thanks it sounds fine in that case. The function is pretty much equivalent to a class, which means we can safely have slightly different opinions about the best way to encapsulate, since they'll behave in roughly the same way :)
There was a problem hiding this comment.
Oh interesting. I wonder if this would be better as a class. I think bp takes the place of a class. But we could make the functions themselves available publicly/directly.
| return flask.json.jsonify(judgements_doc) | ||
|
|
||
| @bp.route("/v1/judgements/<int:judgement_id>/preference", methods=["put"]) | ||
| @util.authorized_user_action( |
There was a problem hiding this comment.
I like the decorator-based security.
| """ | ||
| Sets the preference bit for a specific judgement and returns the event | ||
| """ | ||
| preference = json.loads(request_values['preference']) |
There was a problem hiding this comment.
This could also fail to be well-formed json, what would happen in that case? I think it throws a ValueError, which we should catch.
There was a problem hiding this comment.
Right, I don't do all the error handling when we're here. I think we might want to consider adding a decorator that will handle many error types and return a specific type of structured response. I haven't worked out all of the details for that yet. I'm not sure it will make sense (DRY, KISS) until it is half-implemented.
There was a problem hiding this comment.
Updates made! I put the logic for formatting an exc in the Exception classes themselves. I'm not sure how I feel about that. What do you think?
There was a problem hiding this comment.
It looks really nice to use once implemented!
| return util.execute_and_log_or_error(state, proto_event) | ||
|
|
||
| @bp.route("/v1/judgements/", methods=["post"]) | ||
| @util.authorized_user_action( |
There was a problem hiding this comment.
Per our IRC conversation, maybe we need to check authorization based on the wiki context instead.
There was a problem hiding this comment.
Right. We should be able to access the request values (that include "context") inside of the authorized_user_action decorator.
There was a problem hiding this comment.
That would work, and I guess it's fine to split request parsing, it's a good trade-off in exchange for nice encapsulation of the security aspect.
There was a problem hiding this comment.
Note that authorized_user_action does all of the request pre-processing too (since that's necessary for decrypting data from trusted clients). So in a way, we're just sequencing request parsing. The request_values map roughly mimics flask.request.values with the same accessor methods (MultiDict).
| return decorator | ||
|
|
||
|
|
||
| def execute_and_log_or_error(state, proto_event): |
| gu_id = values['gu_id'] | ||
| elif 'mwoauth_access_token' in flask.session: | ||
| values = flask.request.values | ||
| gu_id = flask.session.get('mwoauth_identity')['id'] |
There was a problem hiding this comment.
I missed how we validated the access token here?
There was a problem hiding this comment.
We don't. This will need to happen in connection with mwoauth. Given that that is easy and we've done it multiple times, I've skipped spec'ing it out.
There was a problem hiding this comment.
Great. Maybe just a comment here saying that it's not really authenticating, so we don't accidentally deploy like this?
There was a problem hiding this comment.
Ohh! Even better, I'll raise NotImplementedError()
There was a problem hiding this comment.
Just noting that this still isn't protected, should raise the NotImplementedError.
|
|
||
| class TrustedClientVerificationError(RuntimeError): | ||
| pass | ||
| class RequestError(RuntimeError): |
There was a problem hiding this comment.
implements JSONFormattable (make an interface)
| centralauth: | ||
| host: https://mediawiki.org | ||
|
|
||
| actions: |
There was a problem hiding this comment.
Put rights inside of action type -- flexibility for other types of constraints.
adamwight
left a comment
There was a problem hiding this comment.
Happy to merge if you want to start a new PR to refine...
| pass | ||
| class RequestError(RuntimeError): | ||
| "An error occured inside of JADE while processing a request" | ||
| TYPE = None |
There was a problem hiding this comment.
Please include codes in the base class, 'unspecified_type' and 'unspecified_subtype' or something. My reasoning is that we might accidentally throw one of these from code or fail to inherit TYPE, and the receiving end won't have certainty that the type and subtype are intentionally missing, vs. lost somehow.
There was a problem hiding this comment.
Hmm. It seems that "None" is the right value here. Missing will cause a crash (AttributeError). We could check to see if the attribute exists before trying to access the value. Ultimately this will need to be represent-able in JSON. None is convenient because None --> null. If we were to use something like NotImplemented, then it would error when converting to JSON.
It seems to me that intentionally missing == not specified and preparing for lost somehow seems overbearing. Can you imagine a scenario where the TYPE or SUBTYPE is lost somehow?
There was a problem hiding this comment.
I see now that you were proposing strings containing "unspecified_type" and "unspecified_subtype". It seems that one could use "not_specified" when they'd like to intentionally not specify whereas intentionally missing would be None/null.
There was a problem hiding this comment.
If you feel strongly about this, no problem. My perspective is just that error handling needs to be absolutely hailstormproof, and leaving the potential for a value to be either string or null is risky for both the server and client. You're right about "unspecified" sending the wrong signal, perhaps "unimplemented_type" would be more clear, if we decide to take this string-only approach?
There was a problem hiding this comment.
The whole point of null/None is mixing it with other types. Are you against the use of null/None in general?
There was a problem hiding this comment.
We can pick this up later, no I'm not opposed to null in general, but in this specific case I am opposed. I'd like the error to be clear and not up for misinterpretation.
| def format_detail(self): | ||
| doc = {'exception': str(self.e)} | ||
| if __debug__: | ||
| ex_type, ex, tb = sys.exc_info() |
There was a problem hiding this comment.
Repeating this code doesn't seem right. Would be better to call a superclass method to get additional debug info, or even have the superclass call into the subclass to get class-specific details.
There was a problem hiding this comment.
:P one liner repeats. We could definitely handle that better.
There was a problem hiding this comment.
Yeah the repetition itself isn't what bothers me, it's that * author of a new error class needs to figure out whether this line will be necessary or not, which is not clear from the code, and * as seen below, it's important to have consistency between these lines.
| doc = {'exception': str(self.e)} | ||
| if __debug__: | ||
| ex_type, ex, tb = sys.exc_info() | ||
| return {'traceback': list(traceback.extract_tb(tb))} |
There was a problem hiding this comment.
Lost the rest of doc here. See the next implementation for correct doc['traceback'] = ...
| return json2multidict(jwt.decode( | ||
| encoded_values, self.key_secrets[auth_key], | ||
| algorithms=[HASH_ALGORITHM])) | ||
| except (jwt.exceptions.InvalidTokenError, |
There was a problem hiding this comment.
I'm not 100% certain about the syntax or whether it makes sense, but we could define this list as a constant.
There was a problem hiding this comment.
This was a pain because not all JWT errors inherit from a common base error type.
$ python
Python 3.5.1+ (default, Mar 30 2016, 22:46:26)
[GCC 5.3.1 20160330] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> error_types = (RuntimeError, ValueError)
>>> try:
... raise RuntimeError()
... except error_types as e:
... print("It worked")
... except Exception as e:
... print("It didn't work")
...
It worked
I can't believe that worked. :D
There was a problem hiding this comment.
lol. Python and Ruby are amazing in that everything is a first-class object.
| def error(e): | ||
| if not isinstance(e, errors.RequestError): | ||
| e = error.UnknownError(e) | ||
| return e.HTTP_CODE, flask.jsonify(e.format_json()) |
There was a problem hiding this comment.
This scares me. Want to wrap it in a try-catch just in case?
There was a problem hiding this comment.
Hey look what I found,
http://flask.pocoo.org/docs/0.12/patterns/apierrors/
If we're using enough of the flask machinery, we can implement the errorhandler decorator to catch RequestErrors.
| return doc | ||
|
|
||
|
|
||
| ############################################################################### |
There was a problem hiding this comment.
I guess this #### should be removed :D
There was a problem hiding this comment.
It's nice for visual separation of different groups of exceptions.
There was a problem hiding this comment.
That could be done by supplying the groups of exceptions in different modules, then including each module here.
| @util.authorized_user_action( | ||
| config, trusted_clients, centralauth, "new_judgement") | ||
| def new_judgement(gu_id, request_values): | ||
| """Creates a new judgement and returns the event""" |
There was a problem hiding this comment.
Per PEP257, it should be "Create" (with trailing s) as action verb
| schema = request_values['schema'] | ||
| data = json.loads(request_values['data']) | ||
|
|
||
| # Check that data represents a valid JSON blob based on the latest |
There was a problem hiding this comment.
Comments are usually bad sign and needed when the code is not explanatory enough, this comment is redundant because "state.schemas.validate(schema, data)" conveys the comment fully.
| # schema version | ||
| state.schemas.validate(schema, data) | ||
|
|
||
| # Construct a new proto-event for the judgement |
|
|
||
| def configure(config, bp, score_processor): | ||
|
|
||
| # /spec/ |
|
|
||
|
|
||
| def execute_and_log_or_error(state, proto_event): | ||
| # Try to execute and log the event |
|
|
||
| @bp.route("/v1/judgements/<string:context>/<string:type>/<int:id>/<string:schema>", methods=["get"]) # noqa | ||
| def get_entity_schema_judgements(context, type_, id_, schema): | ||
| """Gets all judgements for a specific entity schema""" |
There was a problem hiding this comment.
Add period sign at the end (PEP257)
| # Execute the proto_event and construct full event. | ||
| return util.execute_and_log_or_error(state, proto_event) | ||
|
|
||
| @bp.route("/v1/judgements/", methods=["post"]) |
There was a problem hiding this comment.
s/post/POST. I like that it support POST requests only.
| context, type_, id_, schema) | ||
| return flask.json.jsonify(judgements_doc) | ||
|
|
||
| @bp.route("/v1/judgements/<int:judgement_id>/preference", methods=["put"]) |
| return flask.json.jsonify(judgements_doc) | ||
|
|
||
| @bp.route("/v1/judgements/<string:context>/<string:type>", methods=["get"]) | ||
| def get_entity_type_judgements(context, type_): |
There was a problem hiding this comment.
I like trailing _ here, so PEP8y <3
|
Lots and lots of nitpicky stuff, the logic looks okay to me, also would love to see tests :D |
|
@adamwight and/or @Ladsgroup, take another look plz :) |
|
LGTM |
adamwight
left a comment
There was a problem hiding this comment.
This prototype is looking great.
Two broad comments,
- This begs for tests
- http://klen.github.io/py-frameworks-bench/ It might be worth looking at a more efficient mini-framework like falcon over flask, since we're already nicely decoupled from the specific choice of framework.
| @@ -0,0 +1,125 @@ | |||
| # Use lists | |||
There was a problem hiding this comment.
These examples would be better as test case expected outputs. It's unclear what API is being demonstrated here.
| pass | ||
| class RequestError(RuntimeError): | ||
| "An error occured inside of JADE while processing a request" | ||
| TYPE = None |
There was a problem hiding this comment.
We can pick this up later, no I'm not opposed to null in general, but in this specific case I am opposed. I'd like the error to be clear and not up for misinterpretation.
| return doc | ||
|
|
||
|
|
||
| ############################################################################### |
There was a problem hiding this comment.
That could be done by supplying the groups of exceptions in different modules, then including each module here.
|
|
||
| from . import v1 | ||
|
|
||
| PWD = os.path.dirname(os.path.abspath(__file__)) |
There was a problem hiding this comment.
PWD is a confusing name, since it usually refers to the shell's cwd at application launch time, which will not be the same directory we're talking about here. Better to call this WSGI_CODE_DIR or something
| def configure(config, bp, trusted_clients, centralauth, state): | ||
|
|
||
| def configure(bp, trusted_clients, source): | ||
| @bp.route("/v1/judgements", methods=["GET"]) |
There was a problem hiding this comment.
There should be some general TODOs documented, e.g. we'll need result set paging.
| gu_id = values['gu_id'] | ||
| elif 'mwoauth_access_token' in flask.session: | ||
| values = flask.request.values | ||
| gu_id = flask.session.get('mwoauth_identity')['id'] |
There was a problem hiding this comment.
Just noting that this still isn't protected, should raise the NotImplementedError.
No description provided.