Skip to content

Enforce overwriting of parent methods, dynamic creation of API nodes #141

Description

@JB-Wolf

Component level methods should define, in an abstract way, everything exposed by inheriting child.
The child, in some cases, might only be capable of some methods, and not all as @cynthiabour pointed out for sensor.
It would be best to only expose the methods it is capable off.

My suggestion would be (without changing the current Inheritance):

class Sensor(FlowchemComponent):
 def __init__(self,name, hw_device):
  ...
        
 def read(self):
  raise NotImplemented

 def on(self):
  ...
 def off(self):
  ...

This allows for required and optional child methods

Then, a dedicated function could inspect the parent and child for it's methods

def get_routes(instance):
 ownclass      = instance.__class__.__name__
 parent_class = instance.__base__.name__
 #parent class needs some looping probably, to get the actual parent
 
 methods_in_common = list(set(get_methods(ownclass)) & set(get_methods(parent_class)))
 for method in methods_in_common:
# this needs some expansion to allow for PUT GET etc definition
  add_api_route(method)

def get_methods(instance)
 ...
 return methods

The parent methods would be the possible methods, narrowed down by the actual methods (the ones overwritten by the child)
This would avoid having to explicitly define api_routes, since those could be created based on need and capability.
Also it would enforce certain devices top expose certain methods
It would also limit the creation of routes to one layer
Most importantly, it would allow to deduce actually available methods directly from the webserver

Activity

  1. JB-Wolf commented on Jul 24, 2024

    @JB-Wolf
    CollaboratorAuthor

    @mehlert suggests this:

    from types import FunctionType
    import inspect
    
    class FlowchemComponent():
    
        @classmethod
        def insertAPI(cls, api_class, obj):
            print("FlowchemComponent.insertAPI():")
    
            obj_class    = obj.__class__
            api_class_methods = [x for x, y in api_class.__dict__.items() if type(y) == FunctionType and y.__name__ != "__init__"]
    
            parentclasses  = obj.__class__.__mro__
            print("FlowchemComponent.insertAPI(): parentclasses={}".format(parentclasses))
    
            parentclasses_list = list(parentclasses)
            obj_methods = []
            for p in parentclasses_list:
                classname = p.__name__
                if classname != api_class.__name__:
                    obj_methods = obj_methods + [x for x, y in p.__dict__.items() if type(y) == FunctionType and y.__name__ != "__init__"]
    
                print("FlowchemComponent.insertAPI(): p={}".format(p.__name__))
    
            print("FlowchemComponent.insertAPI(): api_class_methods={}".format(api_class_methods))
            print("FlowchemComponent.insertAPI(): obj_methods={}".format(obj_methods))
    
            # check and insert
            for api_method in api_class_methods:
                if api_method in obj_methods:
    
    
                    # do insertion here
                    method = getattr(obj,api_method)
                    argumentsdesc = inspect.getfullargspec(method)
                    print("FlowchemComponent.insertAPI(): argumentsdesc={}".format(argumentsdesc))
                    argsnum = len(argumentsdesc.args)
                    if argsnum == 1:
                        # PUT
                        print("FlowchemComponent.insertAPI(): INSERTION insert PUT api route for '{}.{}'".format(obj_class.__name__, api_method))
                    else:
                        #GET
                        print("FlowchemComponent.insertAPI(): INSERTION insert GET api route for '{}.{}'".format(obj_class.__name__, api_method))
    
    class Sensor(FlowchemComponent):
        def __init__(self):
            print("Sensor.__init__()")
    
            self.insertAPI(Sensor, self)
    
        def method1(self):
            print("Sensor.method1() ERROR")
    
        def method2(self):
            print("Sensor.method2() ERROR")
    
        def method3(self, invalue: str):
            print("Sensor.method3() ERROR")
    
    
    
    class Sensor_A(Sensor):
        def __init__(self):
            print("Sensor_A.__init__()")
            super().__init__() 
            
        def method1(self):
            print("Sensor_A.method1()")
    
        def method2(self):
            print("Sensor_A.method2()")
    
        def method3(self, invalue: str):
            print("Sensor_A.method3()")
    
        def method4(self):
            print("Sensor_A.method4()")
    
    class Sensor_B(Sensor):
        def __init__(self):
            print("Sensor_B.__init__()")
            super().__init__() 
            
        def method2(self):
            print("Sensor_B.method2()")
    
    
    ###########
    
    
    # init
    a = Sensor_A()
    b = Sensor_B()
    
    # call
    a.method1()
    a.method2()
    a.method3("toll")
    a.method4()
    
    b.method1()
    b.method2()
    
    
  2. samuelvitorsaraiva commented on Aug 1, 2024

    @samuelvitorsaraiva
    Collaborator

    To accomplish this idea, the following steps are necessary:

    1. All methods accessible in the API must be included in the component_class (where the methods are overridden). For example:
    class Device(FlowchemDevice):
        ...
        def initialized(self):
            ...
            self.components.append(DeviceComponent("component", self))
    
    class DeviceComponent(FlowchemComponent):   ## All methods accessible in the API must be here!
        def __init__(self):
            print("Sensor.__init__()")
            self.insertAPI(Sensor, self)
    1. The difference between PUT and GET methods must be made clearer.

    If @JB-Wolf and @cynthiabour agree, I can modify these details and implement this auto API route.

  3. cynthiabour commented on Aug 1, 2024

    @cynthiabour
    Contributor

    sorry I don't get how it works. and why this is necessary.

  4. cynthiabour commented on Aug 1, 2024

    @cynthiabour
    Contributor

    In addition, we may need to consider whether all the functionality at component level should be made available to the user. Otherwise, the current approach is not ideal.

  5. JB-Wolf commented on Aug 1, 2024

    @JB-Wolf
    CollaboratorAuthor

    we can specify that by public and private functions.
    It is not really necessary, but
    a) makes maintenances much easier
    b) provides better code structure and separation

  6. mehlert commented on Aug 1, 2024

    @mehlert

    How it works:
    Find all common methods between an interface class (the abstract class) and all child classes (the implementations).
    Decide for GET/PUT route depending on the number of arguments in the signature of the method.

  7. samuelvitorsaraiva commented on Aug 27, 2024

    @samuelvitorsaraiva
    Collaborator

    I create a branch to solve this:
    This works perfectly 👯 . I want to schedule a meeting with the team to see if merging is viable!
    If you see the advantage of this new feature, adding one line of code in every component in flowchem will be necessary!
    The discussion about the difference between GET and PUT remains. If it is to be implemented, the names of some methods in the API server will be the same as those in the class.

  8. cynthiabour commented on Sep 11, 2024

    @cynthiabour
    Contributor

    I have found that some parts of the current code are missing.

    1. Firstly, get and put methods are a pair. For example, 'put flowrate' and 'get fulowrate' share the same endpoint.
    2. we also need to consider that not all get endpoints have no input parameters.
    3. not all functions defined in components need to provide endpoints.

    @samuelvitorsaraiva Please read through the code and find out if it is possible to fix all the above issues.

  9. samuelvitorsaraiva commented on Sep 11, 2024

    @samuelvitorsaraiva
    Collaborator

    Yes
    It's the reason I want to discuss with you all.

    1-It is necessary to have a straightforward way to differentiate between PUTS and GETS. If the number of arguments is reasonable. (Comment on the point 2)

    2-What this function does is basically mirror what is in the class in the API, which means that the name of the API routes will be exactly the same as the class. In this sense, if we have this issue:
    'put flowrate' and 'get flowrate', the API address will mirror the same methods endpoint name, which is in the component class get_flowrate or put_flowrate...

    3-This is done by inspecting the overwrite methods and the option if the user wants to expose the parent methods too. 👯

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

enhancementNew feature or request

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions