class IssuedToken(Document): schema = S.object( properties=S.props( ('token', S.string()), ('user', S.fk('auth','users')), ('exp', S.datetime()), ) )
class Item(Document): schema = S.object( required=['title'], properties=S.props( ("title", S.string(description="The title of the item")), ("complete", S.boolean(default=False)), ("created", S.datetime()), ))
def __new__(mcs, name, bases, attrs): definitions = {} schema = attrs.get('schema', S.object()) # make sure this class inherits definitions and schemas for base in bases: if hasattr(base, "definitions") and base.definitions is not None: definitions = merge(deepcopy(base.definitions), definitions) if hasattr(base, "schema") and base.schema is not None: schema = merge(deepcopy(base.schema), schema) if hasattr(base, "__doc__"): docstring = base.__doc__ # merge definitions from this class into the definition list. if "definitions" in attrs: merge(attrs['definitions'], definitions) else: attrs['definitions'] = definitions # set the title of the schema and class if 'title' not in attrs or (attrs['title'] is None): if 'title' in schema: attrs['title'] = schema['title'] else: attrs['title'] = split_camelcase(name) # use the modified schema attrs['schema'] = schema return super().__new__(mcs, name, bases, attrs)
class Credentials(Document): """A set of credentials for a user""" schema = { 'type': 'object', 'required': ['password', 'salt', 'secret'], 'properties': { 'user': S.fk('api', 'auth', 'users'), 'password': { 'type': 'string', 'description': "The user's (encrypted) password." }, 'salt': { 'type': 'string', 'description': "The salt applied to the password" }, 'secret': { 'type': 'string', 'description': "The user's secret key, used in JWT auth" }, 'jwtClaims': { 'type': 'object', 'description': "Any additional claims to add to a user's JSON Web Token before encoding." }, 'confirmation_code': { 'type': 'string', 'description': "A generated code that confirms a user's email" } } }
schema1 = S.object( title="Ticket", description='A work order for Pronto', required=['title', 'creator', 'status', 'open', 'price'], properties=S.props( ("asset", S.fk('api','core','assets', title="Asset")), ("location", S.geo(description="A copy of asset location, for efficient indexing purposes.", geometry_type='Point')), ("title", S.string(title="Title", description="A description of the title")), ("ticket_type", S.fk('api','core','ticket-types', title="Ticket Type")), ("narrative", S.string(title="Narrative", description="Details relevant to fixing the problem.")), ("confirm_before_dispatch", S.boolean(title="Confirm before dispatch", description="True if 365 pronto should confirm with the asset contact before a worker arrives on site", default=False)), ("clock_running", S.boolean(default=False)), ("next_response_due", S.datetime()), ("inconsistencies", S.integer(default=0, description="The number of inconsistencies reported in answers or status changes.")), ("flags", S.integer(default=0, description="A count of out of bounds values reported in worksheets.")), ("requires_review", S.boolean(default=False)), ("designated_reviewer", S.fk('api','auth','users')), ("related", S.array(items=S.string(), description='Any tickets whose body of work relates to the completion of this ticket.')), ("predecessor", S.string(description='The ticket this ticket was raised as a consequence of.')), ("antecedent", S.string(description='The ticket raised as a consequence of this one.')), ("required_professionals", S.integer(description="The number of people required on this ticket", default=1)), ("assigned_professionals", S.array(items=S.ref('assignee'))), ("creator", S.fk('api','auth','users', description="The person who created the ticket")), ("assignee", S.fk('api','auth','users', description="The person who currently is responsible for the ticket")), ("status", S.ref('ticket_status')), ("tech_support_token", S.string( description="Automatically generated. Send this token as part of a URL in email to allow a third party " "tech support access to view this ticket and communicate with the assigned professionals " "through the admin console or third-party app." )), ("open", S.boolean(default=False)), ("price", S.string()), ("currency", S.string(default='USD')), ("customer_billed", S.datetime()), ("customer_paid", S.datetime()), ("customer_paid_in_full", S.boolean(default=True)), ("contractor_paid", S.datetime()), ("work_requirements", S.array(items=S.ref('work_requirement'))), ("union", S.boolean(description="True if the asset requires a union contractor.")), ("prevailing_wage", S.boolean(description="True if the asset requires prevailing wage.")), ("worksheets", S.array(items=S.ref('worksheets_for_status'))), ("work_performed", S.array(items=S.ref('work'))), ('arbitration_required', S.boolean(default=False)), ('arbitration_complete', S.boolean()), ('result_of_arbitration', S.textarea()), ('linked', S.fk('core', 'tickets', description="This is set if this ticket is listed in another ticket's links")), ('linked_tickets', S.fk_array('core', 'tickets', description="Other tickets that must be complete for this one to be considered finished. These are " "often tickets that are co-located on the same site.")) ))
class User(Document): """A basic, but fairly complete system user record""" active_by_default = True template = "${username}" schema = { 'type': 'object', 'required': ['email'], 'properties': S.props( ('username', { 'title': 'Username', 'type': 'string', 'description': 'The user\'s username', }), ('email', { 'title': 'Email', 'type': 'string', 'description': 'The user\'s email address', 'format': 'email', 'pattern': '^[^@]+@[^@]+\.[^@]+$' }), ('email_verified', { 'title': 'Email Verified', 'type': 'boolean', 'description': 'Whether or not this email address has been verified', }), ('picture', S.image(description='A URL resource of a photograph')), ('family_name', { 'title': 'Family Name', 'type': 'string', 'description': 'The user\'s family name', }), ('given_name', { 'title': 'Given Name', 'type': 'string', 'description': 'The user\'s family name', }), ('names', { 'title': 'Other Names', 'type': 'array', 'items': {'type': 'string'}, 'description': 'A list of names that go between the given name and the family name.', }), ('locale', { 'title': 'Default Language', 'type': 'string', 'description': "The user's locale. Default is en-US", 'default': 'en-US' }), ('active', { 'title': 'Active', 'type': 'boolean', 'description': 'Whether or not the user is currently able to log into the system.', 'default': active_by_default }), ('admin', { 'title': 'Administrator', 'type': 'boolean', 'description': 'If true, this user can access all methods of all APIs.', 'default': False }), ('created', { 'title': 'Created', 'format': 'date-time', 'type': 'string', 'description': 'The timestamp this user was created', }), ("roles", { 'title': 'Roles', "type": "array", "items": S.fk('api', 'auth', 'roles'), "description": "Roles that have been granted to this user", }), ("dob", { "title": "Date of Birth", "type": "string", "format": "date-time", "description": "The user's birthday" }) ) } @expose_method def permissions(self) -> [dict]: return functools.reduce(operator.add, [role['permissions'] for role in self.fetch('roles')], []) @expose_method def confirm_email(self, confirmation_code: str) -> bool: confirmed = self.application['credentials'][self.url]['confirmation_code'] == confirmation_code self['confirmed'] = self['confirmed'] or confirmed self.save() return self['confirmed'] @authorized_method @expose_method def send_confirmation_email(self, _user=None) -> None: raise NotImplemented() def __str__(self): return self['username']
class Document(MutableMapping, metaclass=DocumentMetaclass): """ The base type of an persistent Document, corresponding to one RethinkDB record. Each record is an instance of exactly one document class. To combine schemas and object definitions, you can use Python inheritance normally. Inherit from multiple Document classes to create one Document class whose schema and definitions are combined by reference. Most Document subclasses will define at the very least a docstring, Args: obj: a source Document or dict-like object collection (sondra.collections.Collection, optional): The Collection instance that this document belongs to, if any. from_db (bool=False): Set to true of this was constructed from a stored database object. metadata: Some kinds of queries return metadata about the object. If a db query returned metadata, it will be passed here. Attributes: collection (sondra.collection.Collection): The collection this document belongs to. defaults (dict): The list of default values for this document's properties. title (str): The title of the document schema. Defaults to the case-split name of the class. template (string): A template string for formatting documents for rendering. Can be markdown. schema (dict): A JSON-serializable object that is the JSON schema of the document. definitions (dict): A JSON-serializable object that holds the schemas of all referenced object subtypes. exposed_methods (list): A list of method slugs of all the exposed methods in the document. saved (bool): if this document exists in the database. metadata (dict): A set of metadata from the database about this object (query-dependent) debug_validate_on_retrieval (bool=True): Set at the class derivation level. If when debugging, a validation step should happen when documents are retrieved from the database. """ title = None defaults = {} template = "${id}" display_name_template = "{id}" processors = [] specials = {} store_nulls = set() debug_validate_on_retrieval = False def constructor(self, obj): """ This is the document constructor. It contains the business logic behind building the document from an input document or from the database. Args: obj: the obj passed in __init__ """ if self.collection.primary_key in obj: self._url = '/'.join( (self.collection.url, _reference(obj[self.collection.primary_key]))) if '_url' in obj: del obj['_url'] if '_display_name' in obj: del obj['_display_name'] if obj: for k, v in obj.items(): try: self[k] = v except Exception as e: raise KeyError(k, str(e)) for k in self.defaults: if k not in self: if callable(self.defaults[k]): try: self[k] = self.defaults[k]() except: self[k] = self.defaults[k](self.suite) else: self[k] = self.defaults[k] for k, vh in self.specials.items(): if k not in self: if vh.has_default: self[k] = vh.default_value() for p in self.processors: p.run_on_constructor(self) if self.debug_validate_on_retrieval and self.saved and self.suite.debug: self.validate() def __init__(self, obj, collection=None, from_db=False, metadata=None): self.collection = collection self.saved = from_db self.metadata = metadata or {} self.obj = OrderedDict() if self.collection is not None: self.schema = self.collection.schema # this means it's only calculated once. helpful. else: self.schema = mapjson(lambda x: x(context=self) if callable(x) else x, self.schema) # turn URL references into URLs self._url = None self.constructor(obj) def __str__(self): return self.template.format(**self.obj) def refresh(self): new = self.collection[self.id] self.obj = new.obj return self @property def application(self): """The application instance this document's collection is attached to.""" return self.collection.application @property def suite(self): """The suite instance this document's application is attached to.""" return self.application.suite def display_name(self): return self.display_name_template.format(self.obj) @property def id(self): """The value of the primary key field. None if the value has not yet been saved.""" if self.saved: return self.obj[self.collection.primary_key] else: return None @id.setter def id(self, v): self.obj[self.collection.primary_key] = v self._url = '/'.join((self.collection.url, v)) @property def name(self): return self.id or "<unsaved>" @property def url(self): if self._url: return self._url elif self.collection: return self.collection.url + "/" + self.slug else: return self.slug @property def schema_url(self): return self.url + ";schema" @property def slug(self): """Included for symmetry with application and collection, the same as 'id'.""" return self.id # or self.UNSAVED def __len__(self): """The number of keys in the object""" return len(self.obj) def __eq__(self, other): """True if and only if the primary keys are the same""" if isinstance(other, Document): return self.id and (self.id == other.id) elif isinstance(other, dict): return self.id and (self.id == other[self.collection.primary_key]) else: return self.id == other def __contains__(self, item): return item in self.obj def __getitem__(self, key): """Return either the value of the property or the default value of the property if the real value is undefined""" if isinstance( key, Document ): # handle the case where our primary key is a foreign key and the user passes in the instance. key = key.id if key in self.obj: v = self.obj[key] elif key in self.defaults: v = self.defaults[key] else: raise KeyError(key) if key in self.specials: return self.specials[key].to_python_repr(v, self) else: return v def __hash__(self): return hash(self.id) def fetch(self, key): """Return the value of the property interpreting it as a reference to another document""" if key in self.obj: if isinstance(self.obj[key], list): return [ Reference(self.suite, ref).value for ref in self.obj[key] ] elif isinstance(self.obj[key], dict): return { k: Reference(self.suite, ref).value for k, ref in self.obj[key].items() } if self.obj[key] is not None: return Reference(self.suite, self.obj[key]).value else: return None else: raise KeyError(key) def __setitem__(self, key, value): """Set the value of the property, saving it if it is an unsaved Document instance""" if value is None: if key not in self.store_nulls: if key in self.obj: del self.obj[key] for p in self.processors: p.run_after_set(self, key) else: # if the key needs further processing, e.g. foreign keys, geometry, or dates, process. if key in self.specials: value = self.specials[key].to_json_repr(value, self, bare_keys=True) if value is None: if key in self.obj: del self.obj[key] for p in self.processors: p.run_after_set(self, key) return # use the processed value as the value of the key. self.obj[key] = value # post-process the document after the value changes for p in self.processors: p.run_after_set(self, key) def __delitem__(self, key): del self.obj[key] for p in self.processors: p.run_after_set(self, key) def __iter__(self): return iter(self.obj) def update(*args, **kwargs): self, *args = args # to conform to MutableMapping sig def sub_update(s, v, *k): if len(k) > 1: return sub_update(s[k[0]], v, k[1:]) else: s[k[0]] = v return s for k, v in args: if '.' in k: k0, *ks = k.split('.') self[k0] = sub_update(self[k0], v, *ks) else: self[k] = v for k, v in kwargs.items(): if '.' in k: k0, *ks = k.split('.') self[k0] = sub_update(self[k0], v, *ks) else: self[k] = v self.save() return self.collection[self.id] @expose_method_explicit( title='Related Documents', description= 'Reverse relation. Get a query set of all documents in a collection that have foreign keys that ' 'point to this document.', request_schema=S.object( required=['app', 'coll'], properties=S.props( ('app', S.string( description="The slug of the application ``coll`` is in.") ), ('coll', S.string( description= "The slug of the collection to search for documents in.") ), ('related_key', S.string( description= "The name of the key to search for this document in." "If none, defaults to the first matching foreign key element." )), )), response_schema=S.object(properties=S.props()), ) def rel(self, app, coll, related_key=None, related_index=None): """ Reverse relation. Get a query set of all documents in a collection that have foreign keys that point to this document. Args: app (str): The slug of the application ``coll`` is in. coll (str): The slug of the collection to search for documents in. related_key (:obj:`str`, optional): The name of the key to search for this document in. If none, defaults to the first matching foreign key element. related_index (:obj:`str`, optional): The name of the index to search for this document in. If none, defaults to the first matching foreign key element. Returns: A sondra.collection.QuerySet object Raises: KeyError: If there are no foreign keys to this document's collection specified on the target's schema. An empty result will be just that. This error is only raised when the foreign-key is not specified. """ c = self.suite[app][coll] if related_index: return c.query.get_all(self.id, index=related_index) elif not related_key: for k, v in c.document_class.specials.items(): if isinstance( v, ForeignKey ) and v.app == self.application.slug and v.coll == self.collection.slug: related_key = k break else: raise KeyError( "Cannot find any foreign keys to {0}/{1}".format( app, coll)) return c.filter(**{related_key: self.id}) def raw_rel(self, app, coll, related_key=None): """ Reverse relation. Get a raw query set of all documents in a collection that have foreign keys that point to this document. Args: app (str): The slug of the application ``coll`` is in. coll (str): The slug of the collection to search for documents in. related_key (:obj:`str`, optional): The name of the key to search for this document in. If none, defaults to the first matching foreign key element. Returns: A sondra.collection.RawQuerySet object Raises: KeyError: If there are no foreign keys to this document's collection specified on the target's schema. An empty result will be just that. This error is only raised when the foreign-key is not specified. """ c = self.suite[app][coll] if not related_key: for k, v in c.document_class.specials.items(): if isinstance( v, ForeignKey ) and v.app == self.application.slug and v.coll == self.collection.slug: related_key = k break else: raise KeyError( "Cannot find any foreign keys to {0}/{1}".format( app, coll)) return c.raw_query.filter({related_key: self.id}) def help(self, out=None, initial_heading_level=0): """Return full reStructuredText help for this class""" builder = help.SchemaHelpBuilder( self.schema, self.url, out=out, initial_heading_level=initial_heading_level) builder.begin_subheading(self.name) builder.begin_list() builder.define("Collection", self.collection.url + ';help') builder.define("Schema URL", self.schema_url) builder.define("JSON URL", self.url) builder.end_list() builder.end_subheading() builder.build() if self.exposed_methods: builder.begin_subheading("Methods") for name, method in self.exposed_methods.items(): new_builder = help.SchemaHelpBuilder( method_schema(self, method), initial_heading_level=builder._heading_level) new_builder.build() builder.line(new_builder.rst) return builder.rst def json(self, *args, **kwargs): return json.dumps(self.json_repr(), *args, **kwargs) def rql_repr(self): ret = deepcopy(self.obj) value_handlers = self.specials or {} for k, handler in value_handlers.items(): if k in ret: ret[k] = handler.to_rql_repr(ret[k], self) return ret def json_repr(self, ordered=False, bare_keys=False): js = deepcopy(self.obj) for property, special in self.specials.items(): if property in js: js[property] = special.to_json_repr(js[property], self, bare_keys=bare_keys) # if ordered: # js = natural_order(js, self.property_order) # if self.saved: # js['_url'] = self._url # js['_display_name'] = self.display_name() return js def geojson_repr(self, ordered=False, bare_keys=False): js = self.json_repr(ordered, bare_keys) if 'feature_geometry' in self.schema: geom = js.get(self.schema['feature_geometry'], None) else: GEOM_TYPES = { 'geometry', 'point', 'linestring', 'polygon', 'multipoint', 'multilinestring', 'multipolygon' } geoms = filter( lambda v: isinstance(v, dict) and v.get('type', '').lower() in GEOM_TYPES, js.values()) try: geom = next(geoms) except StopIteration: geom = None # if ordered: # js = natural_order(js, self.property_order) if self.saved: js['_url'] = self._url return {"type": "Feature", "geometry": geom, "properties": js} def save(self, conflict='replace', *args, **kwargs): ret = self.collection.save(self, conflict=conflict, *args, **kwargs) return ret def delete(self, **kwargs): ret = self.collection.delete(self, **kwargs) return ret def validate(self): jsonschema.validate(self.obj, self.schema) @deprecated def pre_save(self): pass @deprecated def post_save(self): pass @deprecated def pre_delete(self): pass @deprecated def post_delete(self): pass
schema1 = S.object( title="Ticket", description='A work order for Pronto', required=['title', 'creator', 'status', 'open', 'price'], properties=S.props( ("asset", S.fk('api', 'core', 'assets', title="Asset")), ("location", S.geo(description= "A copy of asset location, for efficient indexing purposes.", geometry_type='Point')), ("title", S.string(title="Title", description="A description of the title")), ("ticket_type", S.fk( 'api', 'core', 'ticket-types', title="Ticket Type")), ("narrative", S.string(title="Narrative", description="Details relevant to fixing the problem.")), ("confirm_before_dispatch", S.boolean( title="Confirm before dispatch", description= "True if 365 pronto should confirm with the asset contact before a worker arrives on site", default=False)), ("clock_running", S.boolean(default=False)), ("next_response_due", S.datetime()), ("inconsistencies", S.integer( default=0, description= "The number of inconsistencies reported in answers or status changes." )), ("flags", S.integer(default=0, description= "A count of out of bounds values reported in worksheets.")), ("requires_review", S.boolean(default=False)), ("designated_reviewer", S.fk('api', 'auth', 'users')), ("related", S.array( items=S.string(), description= 'Any tickets whose body of work relates to the completion of this ticket.' )), ("predecessor", S.string(description= 'The ticket this ticket was raised as a consequence of.')), ("antecedent", S.string( description='The ticket raised as a consequence of this one.')), ("required_professionals", S.integer(description="The number of people required on this ticket", default=1)), ("assigned_professionals", S.array(items=S.ref('assignee'))), ("creator", S.fk('api', 'auth', 'users', description="The person who created the ticket")), ("assignee", S. fk('api', 'auth', 'users', description="The person who currently is responsible for the ticket" )), ("status", S.ref('ticket_status')), ("tech_support_token", S.string( description= "Automatically generated. Send this token as part of a URL in email to allow a third party " "tech support access to view this ticket and communicate with the assigned professionals " "through the admin console or third-party app.")), ("open", S.boolean(default=False)), ("price", S.string()), ("currency", S.string(default='USD')), ("customer_billed", S.datetime()), ("customer_paid", S.datetime()), ("customer_paid_in_full", S.boolean(default=True)), ("contractor_paid", S.datetime()), ("work_requirements", S.array(items=S.ref('work_requirement'))), ("union", S.boolean( description="True if the asset requires a union contractor.")), ("prevailing_wage", S.boolean(description="True if the asset requires prevailing wage.")), ("worksheets", S.array(items=S.ref('worksheets_for_status'))), ("work_performed", S.array(items=S.ref('work'))), ('arbitration_required', S.boolean(default=False)), ('arbitration_complete', S.boolean()), ('result_of_arbitration', S.textarea()), ('linked', S.fk('core', 'tickets', description= "This is set if this ticket is listed in another ticket's links") ), ('linked_tickets', S.fk_array( 'core', 'tickets', description= "Other tickets that must be complete for this one to be considered finished. These are " "often tickets that are co-located on the same site."))))