Skip to content

Progress towards working /judgements/ path#4

Merged
adamwight merged 5 commits into
masterfrom
wsgi_structure
Dec 18, 2017
Merged

Progress towards working /judgements/ path#4
adamwight merged 5 commits into
masterfrom
wsgi_structure

Conversation

@halfak
Copy link
Copy Markdown
Member

@halfak halfak commented Nov 21, 2017

No description provided.

Comment thread jade/wsgi/routes/v1/judgements.py Outdated
def configure(bp, config, trusted_clients, centralauth, state, stream):

def configure(bp, trusted_clients, source):
@bp.route("/v1/judgements/<string:context>", methods=["get"])
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Swagger might be the best place to experiment with routes...

Comment thread jade/wsgi/routes/v1/judgements.py Outdated

from ... import responses

def configure(bp, config, trusted_clients, centralauth, state, stream):
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 :)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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(
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the decorator-based security.

"""
Sets the preference bit for a specific judgement and returns the event
"""
preference = json.loads(request_values['preference'])
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That sounds great.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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(
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Per our IRC conversation, maybe we need to check authorization based on the wiki context instead.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right. We should be able to access the request values (that include "context") inside of the authorized_user_action decorator.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Comment thread jade/wsgi/util.py
return decorator


def execute_and_log_or_error(state, proto_event):
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like it.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

\o/

Comment thread jade/wsgi/util.py
gu_id = values['gu_id']
elif 'mwoauth_access_token' in flask.session:
values = flask.request.values
gu_id = flask.session.get('mwoauth_identity')['id']
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I missed how we validated the access token here?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great. Maybe just a comment here saying that it's not really authenticating, so we don't accidentally deploy like this?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ohh! Even better, I'll raise NotImplementedError()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ty

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just noting that this still isn't protected, should raise the NotImplementedError.

Comment thread jade/errors.py

class TrustedClientVerificationError(RuntimeError):
pass
class RequestError(RuntimeError):
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

implements JSONFormattable (make an interface)

Comment thread config/00-main.yaml
centralauth:
host: https://mediawiki.org

actions:
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Put rights inside of action type -- flexibility for other types of constraints.

Copy link
Copy Markdown
Collaborator

@adamwight adamwight left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Happy to merge if you want to start a new PR to refine...

Comment thread jade/errors.py
pass
class RequestError(RuntimeError):
"An error occured inside of JADE while processing a request"
TYPE = None
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Member Author

@halfak halfak Nov 27, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The whole point of null/None is mixing it with other types. Are you against the use of null/None in general?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread jade/errors.py Outdated
def format_detail(self):
doc = {'exception': str(self.e)}
if __debug__:
ex_type, ex, tb = sys.exc_info()
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:P one liner repeats. We could definitely handle that better.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread jade/errors.py Outdated
doc = {'exception': str(self.e)}
if __debug__:
ex_type, ex, tb = sys.exc_info()
return {'traceback': list(traceback.extract_tb(tb))}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lost the rest of doc here. See the next implementation for correct doc['traceback'] = ...

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice catch!

Comment thread jade/trusted_clients.py Outdated
return json2multidict(jwt.decode(
encoded_values, self.key_secrets[auth_key],
algorithms=[HASH_ALGORITHM]))
except (jwt.exceptions.InvalidTokenError,
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not 100% certain about the syntax or whether it makes sense, but we could define this list as a constant.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lol. Python and Ruby are amazing in that everything is a first-class object.

Comment thread jade/wsgi/responses.py
def error(e):
if not isinstance(e, errors.RequestError):
e = error.UnknownError(e)
return e.HTTP_CODE, flask.jsonify(e.format_json())
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This scares me. Want to wrap it in a try-catch just in case?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread jade/errors.py
return doc


###############################################################################
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess this #### should be removed :D

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's nice for visual separation of different groups of exceptions.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That could be done by supplying the groups of exceptions in different modules, then including each module here.

Comment thread jade/wsgi/routes/v1/judgements.py Outdated
@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"""
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Per PEP257, it should be "Create" (with trailing s) as action verb

Comment thread jade/wsgi/routes/v1/judgements.py Outdated
schema = request_values['schema']
data = json.loads(request_values['data'])

# Check that data represents a valid JSON blob based on the latest
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread jade/wsgi/routes/v1/judgements.py Outdated
# schema version
state.schemas.validate(schema, data)

# Construct a new proto-event for the judgement
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here


def configure(config, bp, score_processor):

# /spec/
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same

Comment thread jade/wsgi/util.py


def execute_and_log_or_error(state, proto_event):
# Try to execute and log the event
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same

Comment thread jade/wsgi/routes/v1/judgements.py Outdated

@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"""
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/Gets/Get/

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add period sign at the end (PEP257)

Comment thread jade/wsgi/routes/v1/judgements.py Outdated
# Execute the proto_event and construct full event.
return util.execute_and_log_or_error(state, proto_event)

@bp.route("/v1/judgements/", methods=["post"])
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/post/POST. I like that it support POST requests only.

Comment thread jade/wsgi/routes/v1/judgements.py Outdated
context, type_, id_, schema)
return flask.json.jsonify(judgements_doc)

@bp.route("/v1/judgements/<int:judgement_id>/preference", methods=["put"])
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/put/PUT

return flask.json.jsonify(judgements_doc)

@bp.route("/v1/judgements/<string:context>/<string:type>", methods=["get"])
def get_entity_type_judgements(context, type_):
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like trailing _ here, so PEP8y <3

@Ladsgroup
Copy link
Copy Markdown
Member

Lots and lots of nitpicky stuff, the logic looks okay to me, also would love to see tests :D

@halfak
Copy link
Copy Markdown
Member Author

halfak commented Dec 11, 2017

@adamwight and/or @Ladsgroup, take another look plz :)

@Ladsgroup
Copy link
Copy Markdown
Member

LGTM

Copy link
Copy Markdown
Collaborator

@adamwight adamwight left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread example_outputs.json
@@ -0,0 +1,125 @@
# Use lists
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These examples would be better as test case expected outputs. It's unclear what API is being demonstrated here.

Comment thread jade/errors.py
pass
class RequestError(RuntimeError):
"An error occured inside of JADE while processing a request"
TYPE = None
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread jade/errors.py
return doc


###############################################################################
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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__))
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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"])
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There should be some general TODOs documented, e.g. we'll need result set paging.

Comment thread jade/wsgi/util.py
gu_id = values['gu_id']
elif 'mwoauth_access_token' in flask.session:
values = flask.request.values
gu_id = flask.session.get('mwoauth_identity')['id']
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just noting that this still isn't protected, should raise the NotImplementedError.

@adamwight adamwight merged commit 5ae9877 into master Dec 18, 2017
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants