Package: licenseware
Licenseware SDKThis is the licensewarePython3sdk useful for quickly create apps.
The SDK handles the repetetive actions needed for creating an app (file uploads/validation, background events, api routes and more).
It helps you focus on processsing the files needed and creating reports.ContentsLicenseware SDKContentsQuickstartInstallationSDK new version releaseA minimal appWhat is anApp?Set environment variablesAppdeclarationUploaderdeclarationCreating theworker_functionCreating thevalidator_classReportdeclarationCreating theReport componentCreating theReportCustom namespacesEndpoints from simple functionsThemainfileLicenseware CLICreate the app from CLICreate a new uploader from CLICreate a new report from CLICreate a new report component from CLIWorking on SDKLoad testingPUBSUBQuickstartBasic app flow:User sends list of file names;App through the validator function of the uploader checks which files are relevant and returns a list of validated file names;User uploads the actual files, filtered based on the list provided by the uploader (the front-end does this);The uploader receives the files, runs the validation on the actual file contents (before it only had the file names), and sends the valid files to the worker_function;The worker function processes the files and saves them to mongo based on the structure in the serializers;Once the data is fully analyzed, the user can either view data as reports or as editable tables.Here are the steps needed for local development of an app:Install the sdk globally or on a virtual environment (python3.8) with the following command:pip3 install git+https://git@github.com/licenseware/licenseware-sdk-v2.git;Create a github repository following this naming conventionxxxx-service(wherexxxxit's an unique lowercase acronym for the service);Clone the repo for your service;CD in the cloned repo locally;Create a new app with the same name as the github repository:licenseware new-app xxxx-service;Runlicenseware --helpto see more commands;Each app depends on 2 services to run:Authentification service;Registry service;Runing the app withstack-managerwhich make available the Authentification service and Registry service:Click here and follow the stack manager docsIf you have the stack-manager up and running and want to run the app without docker you can start locally with:make run-local(the url will be available athttp://localhost:5000/your-app-id/docs)Running the app withoutstack-manager:make sure you have the default mongo connection string for mongo installed locally (mongodb://localhost:27017/db);if mongo in docker works on MAC M1 then you can use the following make commands:make start-mongo,make stop-mongo,make logs-mongo;you will have mongoexpress running at:http://localhost:8081/.make run-nostack-mongo(this will start the app with only mongodb as a dependency);open urllocalhost:5000/api/docsfor swagger endpoint testing;Running the app without any external dependency:make run-nostack-nomongo(this will start the app without needing mongodb up and running.
It usesmongitato save data.
Mongita doesn't support aggregation pipelines so this method should be avoided.
Also you can't view data with MongoDB Compass or Mongo Express.
);InstallationClone the repo, and install the sdk withmakeinstall-sdkOr install it with pip:pip3installwheel_sdk/licenseware-2.0.0-py3-none-any.whlOr download the sdk wheel fromthis linkand install it with pip.I repository is public you can install it straight from github.pip3installgit+https://git@github.com/licenseware/licenseware-sdk-v2.gitInstall from a specific branchpip3installgit+https://git@github.com/licenseware/licenseware-sdk-v2.git@branch_nameInstall from a specific tagpip3installgit+https://git@github.com/licenseware/licenseware-sdk-v2.git@tag_nameYou can usegit+sshif you have ssh keys configured.
Uninstall withpip3 uninstall licenseware.SDK new version releaseInsetup.pyupdate the package version;Create a tag with that version ex:git tag -a v0.0.11;You can list available tags withgit tag -n;Push created tag withgit push --tagsNow you use pip to install it from that specific tag:pip3installgit+https://git@github.com/licenseware/licenseware-sdk-v2.git@v0.0.11If you want to add more details regarding this package release you canCreate a new releaseClick the linkReleases;ClickDraft a new release;ClickTags;Select latest tag version name;Add title and description for the release;Optionally you can create a wheel for this package:python3setup.pybdist_wheelsdistAnd add it to binaries on the release.A minimal appBellow is a full working example of almost all features the sdk provides.Start the service withdocker-compose up -dimportdatetimefromtrend_app_protectimportwrap_wsgi_appfromflaskimportFlaskfromflask_restximportNamespace,ResourcefrommarshmallowimportSchema,fieldsfromlicenseware.mongodataimportmongodatafromlicenseware.app_builderimportAppBuilderfromlicenseware.common.constantsimport(envs,filters,flags,icons,states)fromlicenseware.endpoint_builderimportEndpointBuilderfromlicenseware.notificationsimportnotify_upload_statusfromlicenseware.report_builderimportReportBuilderfromlicenseware.report_componentsimportBaseReportComponentfromlicenseware.report_components.style_attributesimportstylesfromlicenseware.uploader_builderimportUploaderBuilderfromlicenseware.uploader_validatorimportUploaderValidatorfromlicenseware.utils.loggerimportlogfromlicenseware.schema_namespaceimportSchemaNamespace,MongoCrudfromlicenseware.editable_tableimportEditableTable,metaspecsapp=Flask(__name__)# APPApp=AppBuilder(name='Infrastructure Mapper',description='Overview of devices and networks',flags=[flags.BETA])# UPLOADERS# Here is the worker function# which will process the files in the backgrounddefrv_tools_worker(event_data):# Event data will contain the following information# event_data = {#     'tenant_id': 'the tenant_id from request',#     'filepaths': 'absolute file paths to the files uploaded',#     'event_id': 'the current event_id of the uploaded files',#     'uploader_id': 'the uploader id in our case rv_tools'#     'flask_request':  'data as dict from flask request'# }log.info("Starting working")notify_upload_status(event_data,status=states.RUNNING)log.debug(event_data)# here add the processing file logicnotify_upload_status(event_data,status=states.IDLE)log.info("Finished working")# Here we are defining the validation required for each upload# If overwriting bellow mentioned methods is not necessary you can use `UploaderValidator` directlyclassRVToolsUploaderValidator(UploaderValidator):# If necessary you can overwrite bellow mentioned methods...# def calculate_quota(self, flask_request) -> Tuple[dict, int]:# responsible for calculating quota based on tenant_id and returning a json response, status code# ...# def get_filenames_response(self, flask_request):# responsible for validating filenames and returning a json response, status code# ...# def get_file_objects_response(self, flask_request):#   responsible for validating filenames, their contents and returning a json response, status code# ...rv_tools_validator=RVToolsUploaderValidator(filename_contains=['RV','Tools'],filename_endswith=['.xls','.xlsx'],ignore_filenames=['skip_this_file.csv'],required_input_type="excel",min_rows_number=1,required_sheets=['tabvInfo','tabvCPU','tabvHost','tabvCluster'],required_columns=['VM','Host','OS','Sockets','CPUs','Model','CPU Model','Cluster','# CPU','# Cores','ESX Version','HT Active','Name','NumCpuThreads','NumCpuCores'])# Here we are creating the uploader# Notice we are providing the the validator created up to `validator_class` parameter# `worker_function` will be called when `uploader_id` is triggered# The `uploader_id` event is triggered when files are uploaded to `/uploads/uploader_id/files` routerv_tools_uploader=UploaderBuilder(name="RVTools",uploader_id='rv_tools',description="XLSX export from RVTools after scanning your Vmware infrastructure.",accepted_file_types=['.xls','.xlsx'],validator_class=rv_tools_validator,worker_function=rv_tools_worker,quota_units=1)# Here we are:# - adding the uploader to the main app (uploaders list)# - sending uploader information to registry-serviceApp.register_uploader(rv_tools_uploader)# REPORTSclassVirtualOverview(BaseReportComponent):def__init__(self,title:str,component_id:str,component_type:str):self.title=titleself.component_id=component_idself.component_type=component_typesuper().__init__(**vars(self))defget_data(self,flask_request):match_filters=self.get_mongo_match_filters(flask_request)log.info(match_filters)return['mongo pipeline result']defset_attributes(self):# Short hand based on value_key# See based on component type funcs from: licenseware.report_components.attributesvalue_key_and_icon=[("number_of_devices",icons.SERVERS),("number_of_databases",icons.DATABASE_ROUNDED)]# Set values straight to self.attributesself.attributes=self.build_attributes(value_key_and_icon)# Or raw dict (same results are achived using the method up)attributes={'series':[{'value_description':'Number of devices','value_key':'number_of_devices','icon':'ServersIcon'},{'value_description':'Number of databases','value_key':'number_of_databases','icon':'DatabaseIconRounded'}]}# You can also return attributesreturnattributesdefset_style_attributes(self):# You can set a dictionary directly or return a dict like bellowself.style_attributes={'width':'1/3'}# or import `style_attributes` dataclass# from licenseware.report_components.style_attributes import style_attributes as stylesstyle_attributes=self.build_style_attributes([styles.WIDTH_ONE_THIRD#etc])returnstyle_attributesdefset_allowed_filters(self):# Provide a list of allowed filters for this componentreturn[# You can use the build_filter methodself.build_filter(column="device_name",allowed_filters=[filters.EQUALS,filters.CONTAINS,filters.IN_LIST],visible_name="Device Name",# validate:bool = True # This will check field_name and allowed_filters),# or you can create the dictionary like bellow (disadvantage no autocomplete, no checks){"column":"database_name","allowed_filters":["equals","contains","in_list"],"visible_name":"Database Name"}]virtual_overview=VirtualOverview(title="Overview",component_id="virtual_overview",component_type='summary')# Register component to registry-service (to act as a first class citizen)App.register_report_component(virtual_overview)# Component order is determined by it's position in the listreport_components=[virtual_overview]# Define a report wich holds one or more report componentsvirtualization_details_report=ReportBuilder(name="Virtualization Details",report_id="virtualization_details",description="This report gives you a detailed view of your virtual infrastructure. Deep dive into the infrastructure topology, identify devices with missing host details and capping rules for licensing.",connected_apps=['ifmp-service'],report_components=report_components)App.register_report(virtualization_details_report)# CUSTOM RESTX NAMESPACES# We can add also custom namespaces to main IFMP ApifromflaskimportFlask,requestimportflask_restxasrestxfromflask_restximportResource,Apiimportmarshmallowasmafromlicenseware.commonimportmarshmallow_to_restx_model# import the converter functionapp=Flask(__name__)api=Api(app)classSimpleNestedSchema(ma.Schema):simple_nested_field=ma.fields.String(required=False,metadata={'description':'the description of simple_nested_field'})classSimpleSchema(ma.Schema):simple_field1=ma.fields.String(required=True,metadata={'description':'the description of simple_field1'})simple_nest=ma.fields.Nested(SimpleNestedSchema)# Give it as parameters the flask-restx `Api` or `Namespace` instance and the Marshmallow schemasimple_nest_from_schema=marshmallow_to_restx_model(api,SimpleSchema)@api.route('/marshmallow-simple-nest')classMaSimpleNest(Resource):# Place it where you need a restx model@api.expect(simple_nest_from_schema,validate=True)defpost(self):returnrequest.json# Add it to main app# it will have the same namespace prefix /ifmp/v1/ + ns-prefix/custom-api-routeApp.register_namespace(custom_ns,path='/ns-prefix')# If the namespace defined up it's used on all apps# add it to licenseware sdk in app_builder default routes# EndpointBuilder# Endpoints can be generated from functions or marshmellow schemas# add http method as a prefix to schema or function handler (get_some_data, PostDeviceDataSchema etc)# Here we are using a function to create an endpoint like /custom_endpoint/custom_data_from_mongodefget_custom_data_from_mongo(flask_request):""" Custom documentation """# Some logic herereturn"Some data"custom_func_endpoint=EndpointBuilder(get_custom_data_from_mongo)App.register_endpoint(custom_func_endpoint)# Here we are using a marshmellow schema to generate an endpointclassDeviceData(Schema):classMeta:collection_name=envs.MONGO_COLLECTION_DATA_NAMEmethods=['GET','PUT']tenant_id=fields.Str(required=False)updated_at=fields.Str(required=False)device_name=fields.Str(required=True)device_model=fields.Str(required=False)custom_schema_endpoint=EndpointBuilder(DeviceData)App.register_endpoint(custom_schema_endpoint)# Namespace from marshmallow schema using SchemaNamespace class# Defining our schemaclassUserSchema(Schema):""" Here is some Namespace docs for user """name=fields.Str(required=True)occupation=fields.Str(required=True)# Overwritting mongo crud methodsclassUserOperations(MongoCrud):def__init__(self,schema:Schema,collection:str):self.schema=schemaself.collection=collectionsuper().__init__(schema,collection)defget_data(self,flask_request):query=self.get_query(flask_request)results=mongodata.fetch(match=query,collection=self.collection)return{"status":states.SUCCESS,"message":results},200defpost_data(self,flask_request):query=UserOperations.get_query(flask_request)data=dict(query,**{"updated_at":datetime.datetime.utcnow().isoformat()})inserted_docs=mongodata.insert(schema=self.schema,collection=self.collection,data=data)returninserted_docsdefput_data(self,flask_request):query=self.get_query(flask_request)updated_docs=mongodata.update(schema=self.schema,match=query,new_data=dict(query,**{"updated_at":datetime.datetime.utcnow().isoformat()}),collection=self.collection,append=False)ifupdated_docs==0:return{"status":states.SUCCESS,"message":"Query didn't matched any data"},400return{"status":states.SUCCESS,"message":""},200defdelete_data(self,flask_request):query=self.get_query(flask_request)deleted_docs=mongodata.delete(match=query,collection=self.collection)returndeleted_docs# A restx namespace is generated on instantiationUserNs=SchemaNamespace(schema=UserSchema,collection="CustomCollection",mongo_crud_class=UserOperations,decorators=[])# Adding the namespace generated from schema to our Appuser_ns=UserNs.initialize()App.register_namespace(user_ns)# Editable tables# In the case we need to have on the front-end an datatable which can be modified by the user the `EditableTable` class can help us create a crud workflow from a marshmellow schema# We can provide information about columns using the `metadata` parameter available on marshmellog `fields` object# The metadata dict can hold the following values:# "editable":bool tell front-end if values from this column can be modified by the user# "visible": bool tell front-end if it should render column to be visible to the user# "distinct_key":str ?# "foreign_key":str  ?# Using the method bellow routes will be created with SchemaNamespace classclassDeviceTableSchema(Schema):classMeta:collection=envs.MONGO_COLLECTION_DATA_NAMEmethods=['GET','PUT']_id=fields.Str(required=False,unique=True)tenant_id=fields.Str(required=True)updated_at=fields.Str(required=False)raw_data=fields.Str(required=False,allow_none=True)name=fields.Str(required=True,metadata=metaspecs(editable=True,visible=True))is_parent_to=fields.List(fields.Str(),required=False,allow_none=True,metadata=metaspecs(editable=True,visible=True,distinct_key='name',foreign_key='name'))is_child_to=fields.Str(required=False,allow_none=True,metadata=metaspecs(editable=True,visible=True,distinct_key='name',foreign_key='name'))is_part_of_cluster_with=fields.List(fields.Str(),required=False,allow_none=True,metadata=metaspecs(editable=True,visible=True,distinct_key='name',foreign_key='name'))is_dr_with=fields.List(fields.Str(),required=False,allow_none=True,metadata=metaspecs(editable=True,visible=True,distinct_key='name',foreign_key='name'))capped=fields.Boolean(required=True,allow_none=False,metadata=metaspecs(editable=True))total_number_of_processors=fields.Integer(required=False,allow_none=True,metadata=metaspecs(editable=True))oracle_core_factor=fields.Float(required=False,allow_none=True,metadata=metaspecs(editable=True))devices_editable_table=EditableTable(title="All Devices",schema=DeviceTableSchema)App.register_editable_table(devices_editable_table)# Overwrite editable tables default crud methods from SchemaNamespace# In the case the default crud methods provided by SchemaNamespace class do not fit our case we can overwrite the method needed.# Same schema but with another name to avoid colisionsclassProcessorsTableSchema(DeviceTableSchema):...# custom handling of dataclassInfraService:def__init__(self,schema:Schema,collection:str):self.schema=schemaself.collection=collectiondefreplace_one(self,json_data:dict):#custom handling of json_datareturn["the overwritten put_data method results"]# inherits from MongoCrud and overwrites `put_data` and `get_data` methodsclassProcessorOp(MongoCrud):def__init__(self,schema:Schema,collection:str):self.schema=schemaself.collection=collectionsuper().__init__(schema,collection)defget_data(self,flask_request):returnstr(flask_request)defput_data(self,flask_request):query=self.get_query(flask_request)returnInfraService(schema=ProcessorsTableSchema,collection=envs.MONGO_COLLECTION_DATA_NAME).replace_one(json_data=query)# creating the restx namespaceProcessorNs=SchemaNamespace(schema=ProcessorsTableSchema,collection=envs.MONGO_COLLECTION_DATA_NAME,mongo_crud_class=ProcessorOp# feeding the custom crud class to SchemaNamespace).initialize()# instantiating the editable tablesprocessor_table=EditableTable(title="All Processors",schema=ProcessorsTableSchema,namespace=ProcessorNs# here we provide our custom namespace)# same as up register the editable tableApp.register_editable_table(processor_table)# Call init_app in the flask function factoryApp.init_app(app)# Register app to registry-serviceApp.register_app()# Protect the app with TrendMicro Application Security# app = wrap_wsgi_app(app)if__name__=="__main__":app.run(port=4000,debug=True)# Userid / Tenantid# 3d1fdc6b-04bc-44c8-ae7c-5fa5b9122f1a# dramatiq main:App.broker -p4 --watch ./ --queues odbMake commands:make test- run all unit tests.make dev-docs- this command will start a pdoc3 http server use for viewing and updating documentation for the app created;make docs- this command will generate html docs based on docstrings provided in the app;Documentation generated can be added later to github pages.See more about documentation creation herepdoc3.What is anApp?Each LicensewareApp/Serviceis responsible for:processing files submitted by the user;creating custom reports based on processed data from files.EachAPPhas:one or more uploadersone or more reportsone or more report componentsEachUPLOADERhas:one file validator classone file processing/worker functionEachREPORThas:one or more report componentsreport components can be attached either to app builder instance or to report builder instanceEachREPORT COMPONENThas:one get_data method;one url where data can be accessed;Set environment variablesFist make sure you have set the environment variables:#.envlocal

DEBUG=true
APP_HOST=http://backend.localhost
APP_ID={{ app_id }}
ENVIRONMENT=local
FILE_UPLOAD_PATH=/tmp/lware
LWARE_IDENTITY_USER=testing-service
LWARE_IDENTITY_PASSWORD=testing123
MONGO_CONNECTION_STRING=mongodb://lware:lware-secret@localhost:27017
MONGO_DATABASE_NAME=db
AUTH_SERVICE_URL=http://backend.localhost/auth
REGISTRY_SERVICE_URL=http://backend.localhost/registry-service
USE_BACKGROUND_WORKER=falseUSE_BACKGROUND_WORKERset to false or not present will skip using background server and process the uploaders data straight on request.AppdeclarationAppBuilderclass will be used to define ourApp.
This class will handle:automatic api generation;sending to registry service information aboutuploader_builder,report_builder,report_componentsand others if needed.#app_definition.pyfromlicenseware.app_builderimportAppBuilderfromlicenseware.common.constantsimportflagsifmp_app=AppBuilder(name='Infrastructure Mapper',description='Overview of devices and networks',flags=[flags.BETA])Theifmp_appinstance is now ready to attach other uploaders, reports, report components (or others) usingifmp_app.register_Xmethods.UploaderdeclarationThe uploader is responsible for:validating files received from user;calculating quota for user and sending the appropiate response if quota exceded;uploading files to disk;triggeringworker_functionto process the files in the background;Each uploader needs avalidator_classand aworker_function.Creating theworker_functionHere is the worker function which will process the files in the background.#worker.pyfromlicenseware.notificationimportnotify_upload_statusfromlicenseware.utils.loggerimportlogdefrv_tools_worker(event:dict):log.info("Starting working")log.debug(event)# here add the processing file logicnotify_upload_status(event,status=states.IDLE)log.info("Finished working")Theeventwill be a dictionary with the following contents:{'tenant_id':flask_request.headers.get("Tenantid"),'filepaths':valid_filepaths,'uploader_id':uploader_id,'event_id':event_id,'flask_request':{**flask_body,**flask_headers},'validation_response':'response from validator class'}Based on giveneventtheworker_functionwill process the files.Creating thevalidator_classHere we are defining the validation and quota calculation required for each upload.Create a new class which inherits fromUploaderValidatorand overwritecalculate_quotafunction.
Methodcalculate_quotareceives a flask request as a parameter which can be used to extract files and tenant_id needed for quota calculation.Finally, instantiate the validator class with the required parameters needed for validation.#validator.pyfromtypingimportTuplefromlicenseware.uploader_validatorimportUploaderValidatorclassRVToolsUploaderValidator(UploaderValidator):# Overwrite `calculate_quota`, `get_filenames_response` or `get_file_objects_response` if needed# Otherwise you can just instantiate the class validator from `UploaderValidator`...rv_tools_validator=RVToolsUploaderValidator(filename_contains=['RV','Tools'],filename_endswith=['.xls','.xlsx'],ignore_filenames=['skip_this_file.csv'],required_input_type="excel",min_rows_number=1,required_sheets=['tabvInfo','tabvCPU','tabvHost','tabvCluster'],required_columns=['VM','Host','OS','Sockets','CPUs','Model','CPU Model','Cluster','# CPU','# Cores','ESX Version','HT Active','Name','NumCpuThreads','NumCpuCores'])If parameters provided for validating filenames and contents are not enough
you can also overwrite:calculate_quota,get_filenames_responseandget_file_objects_responsemethods.calculate_quota: given a flask request object calculates quota for tenant_id based on current processing units (by default it's using len(files) got from request)get_filenames_response: given a flask request object validates filenames and returns a json response, status codeget_file_objects_response: given a flask request object validates filenames and contents and returns a json response, status codeNow we haverv_tools_validatoras avalidator_classandrv_tools_workeras aworker_function.#uploader.pyfromlicenseware.uploader_builderimportUploaderBuilderfromlicenseware.uploader_validatorimportUploaderValidatorfrom...workerimportrv_tools_workerfrom...validatorimportrv_tools_validatorrv_tools_uploader=UploaderBuilder(name="RVTools",uploader_id='rv_tools',description="XLSX export from RVTools after scanning your Vmware infrastructure.",accepted_file_types=['.xls','.xlsx'],validator_class=rv_tools_validator,worker_function=rv_tools_worker)Great! Now we have an uploader defined!We can later import the uploader in our main file and register it to our definedApp.
The registering process will take care of api generation for uploaders.ifmp_app.register_uploader(rv_tools_uploader)Of course defining an uploader can be defined in just one file too.ReportdeclarationAReportis composed of one or morereport components.
Each report component will inherit fromBaseReportComponentclass.Creating theReport componentThe following methods will need to be overwrited:get_data: receives a flask request needs to return data for the declared report component;usematch_filters = self.get_mongo_match_filters(flask_request)to get default pipeline filters (tenant_id and filters from front-end);set_attributes: return based oncomponent_typecomponent metadata which is used by front-end to render data received fromget_datamethod;set_style_attributes: return component style metadata which is used by front-end to apply different css attributes  (width, height, color etc);ANotImplmentedErrorexception will raise if methods mentioned up are not overwritted.#some_report_component.pyfromlicenseware.report_componentsimportBaseReportComponentfromlicenseware.report_components.style_attributesimportstyle_attributesasstylesfromlicenseware.common.constantsimporticons# These are v3 implementations of report attributesfromlicenseware.report_componentsimportTableAttrs,SummaryAttrs,StyleAttrsclassVirtualOverview(BaseReportComponent):def__init__(self,title:str,component_id:str,component_type:str):self.title=titleself.component_id=component_idself.component_type=component_typesuper().__init__(**vars(self))defget_data(self,flask_request):match_filters=self.get_mongo_match_filters(flask_request)log.info(match_filters)return['mongo pipeline result']defset_attributes(self):# Short hand based on value_key# See based on component type funcs from: licenseware.report_components.attributesvalue_key_and_icon=[("number_of_devices",icons.SERVERS),("number_of_databases",icons.DATABASE_ROUNDED)]# Set values straight to self.attributesself.attributes=self.build_attributes(value_key_and_icon)# Or raw dict (same results are achived using the method up)attributes={'series':[{'value_description':'Number of devices','value_key':'number_of_devices','icon':'ServersIcon'},{'value_description':'Number of databases','value_key':'number_of_databases','icon':'DatabaseIconRounded'}]}# Or use the v3 implementation of this# Checkout licenseware/report_components/attributesv3 for more infosummary=(SummaryAttrs().attr(value_key="number_of_devices",icon="ServersIcon",).attr(value_key="number_of_databases",icon="DatabaseIconRounded"))attributes=summary.metadata# You can also return attributesreturnattributesdefset_style_attributes(self):# You can set a dictionary directly or return a dict like bellowself.style_attributes={'width':'1/3'}# or import `style_attributes` dataclass# from licenseware.report_components.style_attributes import style_attributes as stylesstyle_attributes=self.build_style_attributes([styles.WIDTH_ONE_THIRD#etc])# Or v3 implementation of style attributesstyle_attributes=StyleAttrs().width_one_third.metadatareturnstyle_attributesdefset_allowed_filters(self):# Provide a list of allowed filters for this componentreturn[# You can use the build_filter methodself.build_filter(column="device_name",allowed_filters=[filters.EQUALS,filters.CONTAINS,filters.IN_LIST],visible_name="Device Name",# validate:bool = True # This will check field_name and allowed_filters),# or you can create the dictionary like bellow (disadvantage no autocomplete, no checks){"column":"database_name","allowed_filters":["equals","contains","in_list"],"visible_name":"Database Name"}]virtual_overview=VirtualOverview(title="Overview",component_id="virtual_overview",component_type='summary')Nowvirtual_overviewis ourreport_component.We can later register the component straight to theApporReportifmp_app.register_report_component(virtual_overview)Creating theReportThe report can hold one or more report components.#report.pyfromlicenseware.report_builderimportReportBuilderfrom...some_report_componentimportvirtual_overviewvirtualization_details_report=ReportBuilder(name="Virtualization Details",report_id="virtualization_details",description="This report gives you a detailed view of your virtual infrastructure.",connected_apps=['ifmp-service'],report_components=[virtual_overview])We can later add the report to our mainAppifmp_app.register_report(virtualization_details_report)Reports api will be handled by theifmp_appinstance.Custom namespacesWe are not restricted using just the apis generated fromAppBuilderwe can add new custom ones toApp/ifmp_app.#some_namespace.pyfromflask_restximportNamespace,Resourcens=Namespace(name="Custom",description="This is a custom namespace with the app prefix")@ns.route("/custom-api-route")classCustomApiRoute(Resource):@ns.doc(id="custom")defget(self):return"custom-api-route"We can later import the namespace created to ourmainfileifmp_app.register_namespace(custom_ns,path='/ns-prefix')ifmp_appwill make sure it will have the app prefix.If the custom namespace created is repetead for all apps consider adding it toapp_builderpackage.You can also use the following CLI command to create a new boilerplate controller:licensewarenew-controllernameThis will create the restx controller and handle the imports.Endpoints from simple functionsClassEndpointBuildercan be used to generate endpoints from simple functions.
The function name will be used to extract the http method and the route path (get_custom_data_from_mongo-->GEThttp-method,/get_custom_data_from_mongopath-route)#func.pyfromlicenseware.endpoint_builderimportEndpointBuilderdefget_custom_data_from_mongo(flask_request):""" Custom documentation """# Some logic herereturn"Some data"custom_func_endpoint=EndpointBuilder(get_custom_data_from_mongo)The function will receive a flask request as a parameter and will be added to/custom_endpoint/get_custom_data_from_mongoLater in ourmainfile:ifmp_app.register_endpoint(custom_func_endpoint)ThemainfileIn the main file or increate_appbuilder function (where Flask is instantiated) we can initialize theAppwithifmp_app.init_app(app)whereappis the Flask instance.Wheninit_appis invoked all endpoinds defined inapp_builderwill be created and registration information will be sent to registry-service ifregister=True. You can also initiate the registration to registry-service process withifmp_app.register_app()fromflaskimportFlaskfrom...app_definitionimportifmp_appfrom...uploaderimportrv_tools_uploaderfrom...some_report_componentimportvirtual_overviewfrom...reportimportvirtualization_details_reportfrom...some_namespaceimportnsascustom_nsfrom...funcimportcustom_func_endpointapp=Flask(__name__)# These can be placed in `app_definition`ifmp_app.register_uploader(rv_tools_uploader)ifmp_app.register_report_component(virtual_overview)ifmp_app.register_report(virtualization_details_report)ifmp_app.register_namespace(custom_ns,path='/ns-prefix')ifmp_app.register_endpoint(custom_func_endpoint)# Just like any other flask extensionifmp_app.init_app(app,register=True)if__name__=="__main__":app.run(port=4000,debug=True)Licenseware CLIThe licenseware sdk provides also some CLI utilities for quick development.
You can invoke the cli with by typing licenseware in the terminal followed by --help for docs.$ licenseware --help
Usage: licenseware [OPTIONS] COMMAND [ARGS]...

  Useful CLI commands for automatic code generation, files and folders
  creation.

Options:
  --install-completion [bash|zsh|fish|powershell|pwsh]
                                  Install completion for the specified shell.
  --show-completion [bash|zsh|fish|powershell|pwsh]
                                  Show completion for the specified shell, to
                                  copy it or customize the installation.

  --help                          Show this message and exit.

Commands:
  build-docs            Build app html docs
  build-sdk-docs        Build licenseware sdk html docs
  create-tests          Create tests from swagger docs Command example: >>...
  new-app               Given app_id build a new app The package structure...
  new-report            Given report_id build a new report The package...
  new-report-component  Given component_id and component_type build a new...
  new-uploader          Given uploader_id build a new uploader The package...
  recreate-files        Recreate files that are needed but missingSee help for a command by specifing the command name followed by --help$ licenseware new-report-component --helpCreate the app from CLICreate the app from the terminal.
Argumentnew-apprequires anapp_id. The id will be placed in the.envfile.licensewarenew-appifmpThe entire app structure will be generated.
├──app
│├──common
││└──__init__.py
│├──controllers
││└──__init__.py
│├──__init__.py
│├──report_components
││└──__init__.py
│├──reports
││└──__init__.py
│├──serializers
││└──__init__.py
│├──services
││└──__init__.py
│├──uploaders
││└──__init__.py
│└──utils
│└──__init__.py
├──app.log
├──CHANGELOG.md
├──cloudformation-templates
│├──odb-api_prod.yml
│└──odb-api.yml
├──deploy
│└──jupyter
│├──docker-compose.yml
│└──requirements.txt
├──docker-compose.yml
├──docker-entrypoint.sh
├──Dockerfile
├──Dockerfile.stack
├──main.py
├──makefile
├──Procfile
├──Procfile.stack
├──README.md
├──requirements-dev.txt
├──requirements-tests.txt
├──requirements.txt
├──setup.py
├──test_files
├──tests
│├──__init__.py
│└──test_starter.py
├──tox.ini
└──version.txt14directories,32filesAll imports will be handled by the CLI when you create a new uploader, report or report_component from the terminal.Create a new uploader from CLIArgumentnew-uploaderneeds auploader_idlicensewarenew-uploaderrv_tools.uploaders
├── __init__.py
└── rv_tools
    ├── __init__.py
    ├── validator.py
    └── worker.pyUploader id will berv_tools. Each uploader has a validator and a worker.
All imports an routes will be handled by the licenseware sdk.
To sparse the logic you can create multiple sub-packages/modules.Create a new report from CLIArgumentnew-reportneeds areport_idlicensewarenew-reportvirtualization_details.reports
├── __init__.py
└── virtualization_details
    ├── __init__.py
    └── virtualization_details.pyReport id will bevirtualization_details.
All imports an routes will be handled by the licenseware sdk.
To sparse the logic you can create multiple sub-packages/modules.Create a new report component from CLIArgumentnew-report-componentneeds acomponent_idand acomponent_type.licensewarenew-report-componentvirtual_overviewsummary.report_components
├── __init__.py
└── virtual_overview
    ├── __init__.py
    └── virtual_overview.pyComponent id will bevirtual_overviewand it's component type will besummary.
All imports an routes will be handled by the licenseware sdk.
To sparse the logic you can create multiple sub-packages/modules.For more information inspect licenseware packageWorking on SDKEach new feature should be placed in a packageAdd only features that apply to all or most appsTODOLoad testingbaton docsbaton-uhttp://localhost/appid/yourendpoint-c10-r10000PUBSUBBasic usage:On the kafka broker side define topics(channels)fromlicensware.pubsubimportTopicfromconfluent_kafka.adminimportAdminClientadmin_client=AdminClient({'bootstrap.servers':'mybroker'})topic=Topic(admin_client)topic.new("user_events")topic.new("app_events")topic.delete("app_events")On the app side define stream producer (publisher)fromlicensware.pubsubimportProducer,TopicType,EventTypefromconfluent_kafkaimportProducerasKafkaProducerproducer_client=KafkaProducer({'bootstrap.servers':'mybroker1,mybroker2'})producer=Producer(producer_client)data_stream={"event_type":EventType.ACCOUNT_CREATED,"tenant_id":None,"etc":"data"}producer.publish(TopicType.USER_EVENTS,data)You can also define a consumer (subscriber)fromconfluent_kafkaimportConsumerasKafkaConsumerfromlicenseware.pubsubimportEventType,TopicType,Consumerconsumer_client=KafkaConsumer({'bootstrap.servers':'mybroker1,mybroker2'})consumer=Consumer(consumer_client)defaccount_created_handler(*args,**kwargs):return"some processed data"consumer.dispatch(EventType.ACCOUNT_CREATED,account_created_handler)# etcif__name__=="__main__":consumer.listen()