Python: added smart api server/client sample
authorjani <jani@asema.com>
Tue, 19 Mar 2019 13:54:18 +0000 (15:54 +0200)
committerjani <jani@asema.com>
Tue, 19 Mar 2019 13:54:18 +0000 (15:54 +0200)
Common/Python/SmartAPI/model/Obj.py
Examples/Python/CustomPropertiesAndClassesSample/smartapi_server_client_sample.py [new file with mode: 0755]

index 98833282e95ad8088111d4ce6876420841944e96..4b8bad2f95de585ef3092db3262f9e1f15c1b0cc 100644 (file)
@@ -1409,7 +1409,6 @@ class Obj(object):
        def getDomain(self):
                return self.getIdentifierUriPart(0)
        
-
        def getSystemIdentifier(self):
                return self.getIdentifierUriPart(1)
        
diff --git a/Examples/Python/CustomPropertiesAndClassesSample/smartapi_server_client_sample.py b/Examples/Python/CustomPropertiesAndClassesSample/smartapi_server_client_sample.py
new file mode 100755 (executable)
index 0000000..d137412
--- /dev/null
@@ -0,0 +1,543 @@
+#!/usr/bin/python
+from SmartAPI.model.ValueObject import ValueObject
+from SmartAPI.factory.Factory import Factory
+from SmartAPI.factory.RequestFactory import RequestFactory
+from SmartAPI.factory.ResponseFactory import ResponseFactory
+from SmartAPI.model.Activity import Activity
+from SmartAPI.model.Entity import Entity
+from SmartAPI.model.Person import Person
+from SmartAPI.common.Tools import Tools
+from SmartAPI.common.NS import NS
+from SmartAPI.common.RESOURCE import RESOURCE
+from SmartAPI.rdf.Variant import Variant
+from rdflib import URIRef
+
+
+"""
+       A demonstration of client - server communication with various way to include data in
+       objects and their properties. Demos of both standard and proprietary properties are shown.
+
+       In this demo uses custom terms in the ontology so to identify this ontology, we need to
+       give it a URL. An ontology URL starts with the domain (http://....) and typically ends
+       with a hash (#). Both forward slash and hash are used, the latter usually to separate
+       ontology terms from the rest of the URL.
+"""
+myDomain = "http://smart-api.io/smart/examples/"
+myOntology = myDomain + "sampleontology#"
+
+
+"""
+       The demo has a "client" and a "server". No network traffic is done though, the server methods
+       are simply called with the body of the message the client creates. In a real system this 
+       would be replaced with and HTTP client and server that extract the payloads. See other demos/samples
+       for tutorials on this. Nevertheless, both the simulated client and the server need some
+       identifier anyways, so make them.
+"""
+clientIdentity = myDomain + "Cclientsample"
+serverIdentity = myDomain + "Cserversample"
+
+
+"""
+       There are four demo cases "served" by one server. This is where Activities come along.
+       Each demo is its own Activity. The code below therefore also shows how the Activities can be used to
+       separate various services on the server without having to change the call path.
+"""
+serverActivity1Identity = myDomain + "Cserversample/sampleactivity1"
+serverActivity2Identity = myDomain + "Cserversample/sampleactivity2"
+serverActivity3Identity = myDomain + "Cserversample/sampleactivity3"
+serverActivity4Identity = myDomain + "Cserversample/sampleactivity4"
+
+
+"""
+       This demo shows how to fetch information about three programmers from a database.
+       Let's call those three programmers Huey, Dewey and Louie. The convention is that
+       each has a semantic identifier. The way the server creates those identifiers is up
+       to the server itself. In this case we use our prefix + the name. In a real server,
+       the identifier scheme could be for instance prefix + database id  or prefix + uuid.
+"""
+personIdentityPrefix = myDomain + "Cserversample/persons/"
+person1Identity = personIdentityPrefix + "Huey"
+person2Identity = personIdentityPrefix + "Dewey"
+person3Identity = personIdentityPrefix + "Louie"
+
+
+"""
+       Like everything in Smart API, "Programmers" are objects. To demonstrate how object
+       classes work, let's assume we will have the following class hierarchy:
+       Obj
+               -> Entity
+                       -> PhysicalEntity
+                               -> Person
+                                       -> Employee
+                                               -> Programmer
+                                               
+       Of these, the two last ones are _not_ in the existing Smart API model so
+       this will show how to use such extended classes.
+"""
+
+
+""" ---- Utility functions ---- """
+
+def createReadRequest(caller_identity, responder_activity_identity, entity):
+       r = RequestFactory().create(caller_identity)
+       a = Activity(responder_activity_identity)
+       a.setMethod(RESOURCE.READ)
+       a.addEntity(entity)
+       r.addActivity(a)
+       return r
+
+
+
+""" ----- Client side methods ------ """
+
+"""
+       Demo 1. Fetch information about all employees.
+"""
+
+class SampleClient1(object):
+       def __init__(self):
+               pass
+       
+       def createRequest(self):
+               """
+                       Creating a request for all Employees is really simple. Create an entity that represents
+                       an employee, attach it to a request and off you go. Because the details are empty except for
+                       the type, the server should read this as "return all objects that match the type". By convention,
+                       the server should return any object that is a Person or is of a class that is the child of Person.
+               """
+               e = Person()
+               """ 
+                       Note how in this example the Employee type is added. First, it is our proprietary class,
+                       therefore use our ontology prefix + Employee as the type. Second, it says addType, not setType.
+                       If you look at the resulting Turtle payload, the object will contain two classes: Person and Employee.
+                       With this info, the server can search for all objects that match the types: everything
+                       that is an Employee or a Person or anything that is the child of a Person.
+                       Note that at this point we have not registered Employee anywhere. Therefore the class
+                       hierarchy may not be known to the server i.e. it does not know that an Employee is a subclass
+                       of a Person. By including both classes, the matching still works. However, without a registered
+                       ontology hierarchy, the search has the side effect of also returning "false positives" i.e.
+                       objects that are Persons but not necessarily Employees. Using "setType" here would fix that
+                       problem.
+               """
+               e.addType(myOntology + "Employee")
+               
+               # now we just create a request and serialize it to get the payload to send. We point
+               # the request to the first activity processor with "serverActivity1Identity"
+               request = createReadRequest(clientIdentity, serverActivity1Identity, e)
+               return Tools.serializeRequest(request)
+               
+               
+       def processResponse(self, response_data_body):
+               print "<<--- Client processing incoming response data >>\n"
+               print response_data_body
+               
+               """
+                       When the response arrives, this transforms the payload back to objects.
+                       We assume these are parsed at least to Entitity level in the class hierarchy 
+                       and the names of the people can then be simply printed with the getName method
+                       (as getName is common to all objects)
+               """
+               response = Tools().parseResponse(response_data_body)
+               
+               # Iterate through the results
+               entities = response.getActivities()[0].getEntities()
+               print "\nFound persons:"
+               for e in entities:
+                       print "Name:", e.getName()
+       
+       
+"""
+       Demo 2. Fetch information about a particular programmer.
+"""
+       
+class SampleClient2(object):
+       def __init__(self):
+               pass
+       
+       def createRequest(self):
+               """
+                       Here, instead of making a "blank" person, we add the identity of the person to the search query.
+                       Now the server should return just one person or none. Note that by convention,
+                       in case an identifier is set for an object, all other filtering is skipped.
+                       It is assumed that the identifier is unique so it must match with just one object.
+                       Any other search rules would have no effect.
+               """
+               e = Person(person2Identity)
+               request = createReadRequest(clientIdentity, serverActivity2Identity, e)
+               return Tools.serializeRequest(request)
+       
+       
+       def processResponse(self, response_data_body):
+               print "<<--- Client processing incoming response data >>\n"
+               print response_data_body
+               response = Tools().parseResponse(response_data_body)
+               entities = response.getActivities()[0].getEntities()
+
+               print "\nFound persons:"
+               for e in entities:
+                       """
+                               In this demo we have a bit more info on the programmers, including their ages and the
+                               number of lines of code they produced. See server side for an explanation on how
+                               these are encoded in.
+                       """
+                       print "Name:", e.getName(), "\n\tAge:", e.getFirst(URIRef(myOntology + "age")).getValue(), "\n\tLines of code:", e.getFirst("linesOfCode").getValue()
+       
+       
+"""
+       Demo 3. Use extended classes to handle information.
+"""
+class SampleClient3(object):
+       def __init__(self):
+               pass
+       
+       def createRequest(self):
+               """
+                       In the previous examples we used a generic Person class or manually added the "Employee" type into the
+                       entity. With larger programs this becomes tedious or error prone. A better way is to use a class
+                       that have been derived (inherited) from the Person. See below how a "Programmer" has been defined
+                       as a Python class that inherits "Person" and "Employee"
+               """
+               e = Programmer(person3Identity)
+               request = createReadRequest(clientIdentity, serverActivity3Identity, e)
+               return Tools.serializeRequest(request)
+       
+       
+       def processResponse(self, response_data_body):
+               print "<<--- Client processing incoming response data >>\n"
+               print response_data_body
+               response = Tools().parseResponse(response_data_body, custom_classes = { myOntology + "Employee": Employee, myOntology + "Programmer": Programmer })
+               programmers = response.getActivities()[0].getEntities()
+
+               """
+                       Once we have the Programmer objects, they have helper methods such as "getAge" to display the age. This makes listing
+                       data easier in the code.
+               """
+               print "\nFound persons:"
+               for p in programmers:
+                       # Note: There is a bug in the Smart API Tools class, make a pull from the repository to get rid of 
+                       # the error this line causes
+                       print "Name:", p.getName(), "\n\tAge:", p.getAge().getValue(), "\n\tLines of code:", p.getLinesOfCodeProduced().getValue()
+
+
+"""
+       Demo 4. Use filtering rules in the queries
+"""
+class SampleClient4(object):
+       def __init__(self):
+               pass
+       
+       def createRequest(self):
+               """
+                       In addition to the type, any property can be used to filter data. If a value is set in a read operation,
+                       this means it should be used as a filter value. In this case we'd like to find all programmers between ages
+                       20 and 30. To do so, attach a ValueObject which has a minimum and maximum. Note that in SmartAPI objects,
+                       all property values are wrapped inside a Variant class. So this becomes a structure where the minimum
+                       valus is wrapped in a ValueObject which is wrapped in a Variant.
+               """
+               e = Programmer()
+               v = ValueObject()
+               v.setMinimum(20)
+               v.setMaximum(30)
+               e.setAge(v)
+               request = createReadRequest(clientIdentity, serverActivity4Identity, e)
+               return Tools.serializeRequest(request)
+               
+               
+       def processResponse(self, response_data_body):
+               """
+                       The response processing is the same as in the previous example.
+               """
+               print "<<--- Client processing incoming response data >>\n"
+               print response_data_body
+               response = Tools().parseResponse(response_data_body, custom_classes = { myOntology + "Employee": Employee, myOntology + "Programmer": Programmer })
+               entities = response.getActivities()[0].getEntities()
+
+               print "\nFound persons:"
+               for e in entities:
+                       print "Name:", e.getName(), "\n\tAge:", e.getAge().getValue(), "\n\tLines of code:", e.getLinesOfCodeProduced().getValue()
+                       
+                       
+       
+""" ----- Server side methods ------ """
+
+# -----
+# Demo 1 (see client side for an explanation on what this demo should do)
+class SampleServerActivityProcessor1(object):
+       def __init__(self):
+               pass
+       
+       def processActivity(self, request_activity):
+               
+               # For the client to recognize from which activity this result is, the response
+               # activity will carry the same identifier that was in the request
+               response_activity = Activity(request_activity.getIdentifierUri())
+               
+               # Let's perform some (dummy) filtering of values
+               for entity in request_activity.getEntities():
+                       if not entity.hasIdentifierUri():
+                               """
+                                       The way results are obtained is up to the server. This could
+                                       for instance be a result from a database query,
+                               """
+                               results = [
+                                       {'name': 'Huey', 'age': 25, 'lines_of_code': 1100},
+                                       {'name': 'Dewey', 'age': 21, 'lines_of_code': 955},
+                                       {'name': 'Louie', 'age': 32, 'lines_of_code': 1006}]
+                               
+                               """
+                                       For each result, create an Entity that represents it. Set
+                                       a name for it and attach it to the Activity that is sent
+                                       back carrying the data.
+                               """
+                               for r in results:
+                                       # Give each Entity a proper identity
+                                       resp_entity = Entity(personIdentityPrefix + r['name'])
+                                       # And put in the name
+                                       resp_entity.setName(r['name'])
+                                       response_activity.addEntity(resp_entity)
+                       else:
+                               pass
+                       
+               return response_activity
+       
+       
+# -----
+# Demo 2 (see client side for an explanation on what this demo should do)
+class SampleServerActivityProcessor2(object):
+       def __init__(self):
+               pass
+       
+       def processActivity(self, request_activity):
+               response_activity = Activity(request_activity.getIdentifierUri())
+               
+               for entity in request_activity.getEntities():
+                       if entity.hasIdentifierUri():
+                               print "** Searching for a person with identifier " + entity.getIdentifierUri() + " **\n"
+                               results = [{'name': 'Dewey', 'age': 21, 'lines_of_code': 955}]
+       
+                               """
+                                       In this example we're adding in addition to a name also the age and the number of lines
+                                       of code the programmer has produced to the result. However, it is assumed at this stage
+                                       of the demo that these two properties have not been modeled into an object and a setter
+                                       method is missing. So how do we set those two properties?
+                                       To do that, you can use the "add" method available in each object. It takes as parameters
+                                       the name of the property and the value. So what should the name be? In the example above,
+                                       age is assumed to be properly formally defined and includes the domain URI as a prefix.
+                                       LinesOfCode on the other hand is treated informally just like any parameter.
+                               """
+                               for r in results:
+                                       resp_entity = Entity(personIdentityPrefix + r['name'])
+                                       resp_entity.setName(r['name'])
+                                       
+                                       # This is the proper way: put in the URI prefix _and_ define age in
+                                       # some ontology so that it is explained. Add that ontology into the data designer.
+                                       # And if someone has already defined a concept called "age" in the designer, use that,
+                                       # don't reinvent the wheel.
+                                       resp_entity.add(URIRef(myOntology + "age"), r['age'])
+                                       
+                                       # The following is perfectly legal code and Smart API library will parse it
+                                       # without problems. It is however bad design as there is no explanation anywhere
+                                       # what "linesOfCode" actually is and means.
+                                       resp_entity.add("linesOfCode", r['lines_of_code'])
+                                       response_activity.addEntity(resp_entity)
+                       else:
+                               pass
+                       
+               return response_activity
+       
+
+# -----
+# Demo 3 (see client side for an explanation on what this demo should do)
+class SampleServerActivityProcessor3(object):
+       def __init__(self):
+               pass
+       
+       def processActivity(self, request_activity):
+               response_activity = Activity(request_activity.getIdentifierUri())
+               for entity in request_activity.getEntities():
+                       if entity.hasIdentifierUri():
+                               print "** Searching for a person with identifier " + entity.getIdentifierUri() + " **\n"
+                               results = [{'name': 'Louie', 'age': 32, 'lines_of_code': 1006}]
+                               
+                               """
+                                       In this example we are using a proper extended class called Programmer. This
+                                       class takes care of all the prefixes and property naming. All that is needed
+                                       here is to use the setters to plug in the values and off we go.
+                               """
+                               for r in results:
+                                       resp_entity = Programmer(personIdentityPrefix + r['name'])
+                                       resp_entity.setName(r['name'])
+                                       resp_entity.setAge(r['age'])
+                                       resp_entity.setLinesOfCodeProduced(r['lines_of_code'])
+                                       response_activity.addEntity(resp_entity)
+                       else:
+                               pass
+                       
+               return response_activity
+
+
+# -----
+# Demo 4 (see client side for an explanation on what this demo should do)
+class SampleServerActivityProcessor4(object):
+       def __init__(self):
+               pass
+       
+       def processActivity(self, request_activity):
+               response_activity = Activity(request_activity.getIdentifierUri())
+               for entity in request_activity.getEntities():
+                       """
+                               This is the server side processing of filtering rule. Uses the same
+                               predefined objects as the previous example.
+                       """
+                       if not entity.hasIdentifierUri():
+                               
+                               minimum_age = entity.getAge().getValue().getMinimum().getValue()
+                               maximum_age = entity.getAge().getValue().getMaximum().getValue()
+                               # do some database query of programmers between age limits here
+                               
+                               print "** Searching for persons with age between " + str(minimum_age) + " and " + str(maximum_age) + " **\n"
+                               results = [
+                                       {'name': 'Huey', 'age': 25, 'lines_of_code': 1100},
+                                       {'name': 'Dewey', 'age': 21, 'lines_of_code': 955}]                                     
+                               for r in results:
+                                       resp_entity = Programmer(personIdentityPrefix + r['name'])
+                                       resp_entity.setName(r['name'])
+                                       resp_entity.setAge(r['age'])
+                                       resp_entity.setLinesOfCodeProduced(r['lines_of_code'])
+                                       response_activity.addEntity(resp_entity)
+                       else:
+                               pass
+                       
+               return response_activity
+       
+
+"""
+       This is a sample dummy "server" class that parses the incoming 
+"""
+class SampleServer(object):
+       def __init__(self):
+               pass
+       
+       def processRequest(self, input_data):
+               print "--->> Server processing incoming request data <<\n"
+               print input_data[0]
+               
+               # The body of the request is in input_data[0], the content_type header is in input_data[1]
+               # Read the data
+               request = Tools().parseRequest(input_data[0], input_data[1], custom_classes = { myOntology + "Employee": Employee, myOntology + "Programmer": Programmer })
+               
+               # Create a response to send back. Do it here so that data from different activities can be
+               # attached to it
+               response = ResponseFactory.create(serverIdentity)
+               
+               for activity in request.getActivities():
+                       if activity.getIdentifierUri() == serverActivity1Identity:
+                               response_processor = SampleServerActivityProcessor1()
+                       elif activity.getIdentifierUri() == serverActivity2Identity:
+                               response_processor = SampleServerActivityProcessor2()
+                       elif activity.getIdentifierUri() == serverActivity3Identity:
+                               response_processor = SampleServerActivityProcessor3()
+                       elif activity.getIdentifierUri() == serverActivity4Identity:
+                               response_processor = SampleServerActivityProcessor4()
+                       
+                       response_activity = response_processor.processActivity(activity)
+                       response.addActivity(response_activity)
+               
+               payload, content_type = Tools().serializeResponse(response)
+               return payload
+       
+
+
+""" ---- Sample extension classes ---- """
+
+"""
+       These two classes show how to extend a Smart API class and the mandatory methods needed in the process
+"""
+class Employee(Person):
+       def __init__(self, uri = None):
+               # The init must always init the super class
+               Person.__init__(self, uri)
+               
+               # The subclass should have a type (note how it is set, not added)
+               self.setType(myOntology + "Employee")
+               
+               # For each property, the initialization should run init_property that has
+               # - the prefixed identifier of the property
+               # - the class internal name of the property
+               # - function references to the exists function and getter
+               self.init_property(myOntology + "age", 'age', self.hasAge, self.getAge)
+
+       """
+               For each property, three methods should be implemented
+               - the value exists method ("hasXXX")
+               - the getter ("getXXX")
+               - the setter ("setXXX")
+       """
+       def hasAge(self):
+               return self.age is not None
+       
+       def getAge(self):
+               return self.age
+       
+       def setAge(self, a):
+               if not isinstance(a, Variant):
+                       a = Variant(a)
+               self.age = a
+               
+       # To be able to parse incoming data, each class also needs a parser method that uses the
+       # parse_property method for each custom property.
+       def _parseStatement(self, statement, custom_classes = None):
+               self.parse_property(statement, myOntology + "age", self.setAge, Variant, custom_classes = custom_classes)
+               super(Employee, self)._parseStatement(statement, custom_classes = custom_classes)
+
+
+class Programmer(Employee):
+       def __init__(self, uri = None):
+               Employee.__init__(self, uri)
+               self.setType(myOntology + "Programmer")
+               self.init_property(myOntology + "linesOfCodeProduced", 'linesOfCodeProduced', self.hasLinesOfCodeProduced, self.getLinesOfCodeProduced)
+
+       def hasLinesOfCodeProduced(self):
+               return self.linesOfCodeProduced is not None
+       
+       def getLinesOfCodeProduced(self):
+               return self.linesOfCodeProduced
+       
+       def setLinesOfCodeProduced(self, l):
+               if not isinstance(l, Variant):
+                       l = Variant(l)
+               self.linesOfCodeProduced = l
+               
+       def _parseStatement(self, statement, custom_classes = None):
+               self.parse_property(statement, myOntology + "linesOfCodeProduced", self.setLinesOfCodeProduced, Variant, custom_classes = custom_classes)
+               super(Programmer, self)._parseStatement(statement, custom_classes = custom_classes)
+               
+               
+def print_example_header(example_name):
+       print "\n\n*******************************"
+       print "       ", example_name
+       print "*******************************"
+       
+       
+def main():
+       server = SampleServer()
+       client1 = SampleClient1()
+       client2 = SampleClient2()
+       client3 = SampleClient3()
+       client4 = SampleClient4()
+       
+       print_example_header("Example 1")
+       client1.processResponse(server.processRequest(client1.createRequest()))
+
+       print_example_header("Example 2")
+       client2.processResponse(server.processRequest(client2.createRequest()))
+       
+       print_example_header("Example 3")
+       client3.processResponse(server.processRequest(client3.createRequest()))
+       
+       print_example_header("Example 4")
+       client4.processResponse(server.processRequest(client4.createRequest()))
+       
+
+if __name__ == '__main__':
+       main()