Module licenseware.feature_builder.feature_builder
FeatureBuilder
class can be used to instantiate plugins/add-ons with their own routes and quota.
Usage:
Features can be pre-created in the sdk in the features module, like bellow.
from .feature_builder import FeatureBuilder
PRODUCT_REQUESTS = FeatureBuilder(
name="Product Requests",
description="Allow users request products by sending emails",
access_levels=['admin'],
monthly_quota=10
)
You can also create the feature in the commons/features module in the app.
from licenseware.feature_builder.features import PRODUCT_REQUESTS # or from where you created the feature
App = AppBuilder(
name = 'Plugins',
description = '',
flags = [flags.BETA]
)
# Attach the feature to the app so the routes will be generated automatically
App.register_feature(PRODUCT_REQUESTS)
On a get request to /appid/features
you will get a json object like bellow:
{
"available_features": [
{
"app_id": "plugins",
"name": "Product Requests",
"description": "Allow users request products by sending emails",
"access_levels": [
"admin"
],
"monthly_quota": 10,
"activated": false,
"feature_id": "product_requests_feature",
"feature_path": "/product-requests"
}
],
"initialized_features": []
}
To activate/deactivate the feature:
- make a POST request to /features/feature_path
with the payload {"activated": true}
(to activate the feature/addon)
- make a POST request to /features/feature_path
with the payload {"activated": false}
(to deactivate the feature/addon)
Another get request to /features
path will show the following result:
{
"available_features": [
{
"app_id": "plugins",
"name": "Product Requests",
"description": "Allow users request products by sending emails",
"access_levels": [
"admin"
],
"monthly_quota": 10,
"activated": true,
"feature_id": "product_requests_feature",
"feature_path": "/product-requests"
}
],
"initialized_features": [
{
"name": "Product Requests",
"tenant_id": "0be6c669-ab99-41e9-9d88-753a8fcc4cf8",
"access_levels": [
"admin"
],
"app_id": "plugins",
"description": "Allow users request products by sending emails",
"feature_id": "product_requests_feature",
"feature_path": "/product-requests",
"monthly_quota": 10
}
]
}
Now initialized_features
is filled with the feature we activated.
To see more info about the feature we can make a GET request to /features/feature_path
The follwing result will be returned:
{
"name": "Product Requests",
"tenant_id": "0be6c669-ab99-41e9-9d88-753a8fcc4cf8",
"access_levels": [
"admin"
],
"activated": true,
"app_id": "plugins",
"description": "Allow users request products by sending emails",
"feature_id": "product_requests_feature",
"monthly_quota": 10
}
We've seen how to activate/deactivate features and see more info related to the feature.
We can also update quota for the feature. We just need the import the instantiated feature and call update_quota
method.
Bellow it's a trimmed down example on how we can keep track of a feature usage.
from flask import request
from flask_restx import Resource, Namespace, fields
from licenseware.mongodata import collection
from licenseware.feature_builder.features import PRODUCT_REQUESTS
ns = Namespace(
name="Product requests controler",
description="Send a request message to admins",
path='/requests'
)
model = ns.model("model", dict(
message_request = fields.String(required=True)
))
@ns.route("")
class ProductRequest(Resource):
@ns.expect(model, validate=True)
def post(self):
response, status_code = PRODUCT_REQUESTS.update_quota(request, 1)
if status_code != 200:
return response, status_code
with collection("Data") as col:
col.insert_one(request.json)
return "Request added", 200
Once the quota is being used another GET request to /features/feature_path
will show more info about the quota.
{
"name": "Product Requests",
"tenant_id": "0be6c669-ab99-41e9-9d88-753a8fcc4cf8",
"access_levels": [
"admin"
],
"activated": true,
"app_id": "plugins",
"description": "Allow users request products by sending emails",
"feature_id": "product_requests_feature",
"monthly_quota": 10,
"monthly_quota_consumed": 2,
"quota_reset_date": "2022-03-16T08:18:20.713437"
}
Expand source code
"""
`FeatureBuilder` class can be used to instantiate plugins/add-ons with their own routes and quota.
Usage:
Features can be pre-created in the sdk in the features module, like bellow.
```py
from .feature_builder import FeatureBuilder
PRODUCT_REQUESTS = FeatureBuilder(
name="Product Requests",
description="Allow users request products by sending emails",
access_levels=['admin'],
monthly_quota=10
)
```
You can also create the feature in the commons/features module in the app.
```py
from licenseware.feature_builder.features import PRODUCT_REQUESTS # or from where you created the feature
App = AppBuilder(
name = 'Plugins',
description = '',
flags = [flags.BETA]
)
# Attach the feature to the app so the routes will be generated automatically
App.register_feature(PRODUCT_REQUESTS)
```
On a get request to `/appid/features` you will get a json object like bellow:
```json
{
"available_features": [
{
"app_id": "plugins",
"name": "Product Requests",
"description": "Allow users request products by sending emails",
"access_levels": [
"admin"
],
"monthly_quota": 10,
"activated": false,
"feature_id": "product_requests_feature",
"feature_path": "/product-requests"
}
],
"initialized_features": []
}
```
To activate/deactivate the feature:
- make a POST request to `/features/feature_path` with the payload `{"activated": true}` (to activate the feature/addon)
- make a POST request to `/features/feature_path` with the payload `{"activated": false}` (to deactivate the feature/addon)
Another get request to `/features` path will show the following result:
```json
{
"available_features": [
{
"app_id": "plugins",
"name": "Product Requests",
"description": "Allow users request products by sending emails",
"access_levels": [
"admin"
],
"monthly_quota": 10,
"activated": true,
"feature_id": "product_requests_feature",
"feature_path": "/product-requests"
}
],
"initialized_features": [
{
"name": "Product Requests",
"tenant_id": "0be6c669-ab99-41e9-9d88-753a8fcc4cf8",
"access_levels": [
"admin"
],
"app_id": "plugins",
"description": "Allow users request products by sending emails",
"feature_id": "product_requests_feature",
"feature_path": "/product-requests",
"monthly_quota": 10
}
]
}
```
Now `initialized_features` is filled with the feature we activated.
To see more info about the feature we can make a GET request to `/features/feature_path`
The follwing result will be returned:
```json
{
"name": "Product Requests",
"tenant_id": "0be6c669-ab99-41e9-9d88-753a8fcc4cf8",
"access_levels": [
"admin"
],
"activated": true,
"app_id": "plugins",
"description": "Allow users request products by sending emails",
"feature_id": "product_requests_feature",
"monthly_quota": 10
}
```
We've seen how to activate/deactivate features and see more info related to the feature.
We can also update quota for the feature. We just need the import the instantiated feature and call `update_quota` method.
Bellow it's a trimmed down example on how we can keep track of a feature usage.
```py
from flask import request
from flask_restx import Resource, Namespace, fields
from licenseware.mongodata import collection
from licenseware.feature_builder.features import PRODUCT_REQUESTS
ns = Namespace(
name="Product requests controler",
description="Send a request message to admins",
path='/requests'
)
model = ns.model("model", dict(
message_request = fields.String(required=True)
))
@ns.route("")
class ProductRequest(Resource):
@ns.expect(model, validate=True)
def post(self):
response, status_code = PRODUCT_REQUESTS.update_quota(request, 1)
if status_code != 200:
return response, status_code
with collection("Data") as col:
col.insert_one(request.json)
return "Request added", 200
```
Once the quota is being used another GET request to `/features/feature_path` will show more info about the quota.
```json
{
"name": "Product Requests",
"tenant_id": "0be6c669-ab99-41e9-9d88-753a8fcc4cf8",
"access_levels": [
"admin"
],
"activated": true,
"app_id": "plugins",
"description": "Allow users request products by sending emails",
"feature_id": "product_requests_feature",
"monthly_quota": 10,
"monthly_quota_consumed": 2,
"quota_reset_date": "2022-03-16T08:18:20.713437"
}
```
"""
from typing import List
from flask import Request
from licenseware import mongodata
from licenseware.quota import Quota
from licenseware.common.constants import envs
from licenseware.common.serializers import FeaturesSchema
from licenseware.tenants.user_utils import current_user_has_access_level
from licenseware.utils.logger import log
class FeatureBuilder:
def __init__(self,
name: str,
description: str = None,
access_levels: List[str] = None,
monthly_quota: int = 1,
activated: bool = False,
feature_id: str = None,
feature_path: str = None
):
self.app_id = envs.APP_ID
self.name = name
self.description = description
self.access_levels = access_levels
self.monthly_quota = monthly_quota
self.activated = activated
self.feature_id = feature_id
self.feature_path = feature_path
self.get_details()
def get_details(self):
# ! There can't be 2 features with the same `name`
# `decorators` will be applied on the route created
# `access_levels` will be verified with auth
# Ex:
# access_levels = ['admin'] will check
# `shared_tenant` table `access_level` column for `admin` value
# or if user is the tenant owner
if self.feature_id is None:
self.feature_id = self.name.lower().replace(" ", "_") + '_feature'
if self.feature_path is None:
self.feature_path = '/' + \
self.feature_id.replace("_", "-").replace('-feature', "")
return {
'app_id': self.app_id,
'name': self.name,
'description': self.description,
'access_levels': self.access_levels,
'monthly_quota': self.monthly_quota,
'activated': self.activated,
'feature_id': self.feature_id,
'feature_path': self.feature_path
}
def feature_is_activated(self, tenant_id: str):
feature_activated = mongodata.document_count(
match={
"tenant_id": tenant_id,
"feature_id": self.feature_id,
"activated": True
},
collection=envs.MONGO_COLLECTION_FEATURES_NAME
)
return True if feature_activated else False
def update_quota(self, flask_request: Request, units: int = 1):
tenant_id = flask_request.headers.get("TenantId")
if not self.feature_is_activated(tenant_id):
return "Feature is not activated. Can't update quota", 400
q = Quota(
tenant_id=tenant_id,
auth_token=flask_request.headers.get("Authorization"),
units=self.monthly_quota,
uploader_id=self.feature_id
)
res, status_code = q.check_quota(units)
if status_code == 200:
return q.update_quota(units)
return res, status_code
def get_status(self, flask_request: Request):
tenant_id = flask_request.headers.get("TenantId")
results = mongodata.fetch(
match=({'tenant_id': tenant_id, "name": self.name},
{"_id": 0, "feature_path": 0}),
collection=envs.MONGO_COLLECTION_FEATURES_NAME
)
if not results:
return {}, 200
quotas = mongodata.fetch(
match=({'tenant_id': tenant_id, "uploader_id": self.feature_id}, {
"_id": 0, "feature_path": 0}),
collection=envs.MONGO_COLLECTION_UTILIZATION_NAME
)
if quotas:
results[0]['monthly_quota_consumed'] = quotas[0]['monthly_quota_consumed']
results[0]['quota_reset_date'] = quotas[0]['quota_reset_date']
return results[0], 200
def set_status(self, tenant_id: str, status: bool):
self.activated = status
feature_details = self.get_details()
feature_details['tenant_id'] = tenant_id
updated = mongodata.update(
schema=FeaturesSchema,
match={'tenant_id': tenant_id, 'name': self.name},
new_data=feature_details,
collection=envs.MONGO_COLLECTION_FEATURES_NAME
)
return updated
def update_status(self, flask_request: Request):
status = flask_request.json['activated']
tenant_id = flask_request.headers.get("TenantId")
resp = f"Feature {'activated' if status else 'deactivated'}", 200
if len(self.access_levels) == 0:
self.set_status(tenant_id, status)
return resp
if current_user_has_access_level(flask_request, self.access_levels):
self.set_status(tenant_id, status)
return resp
return "Not enough rights to activate/deactivate feature", 401
Classes
class FeatureBuilder (name: str, description: str = None, access_levels: List[str] = None, monthly_quota: int = 1, activated: bool = False, feature_id: str = None, feature_path: str = None)
-
Expand source code
class FeatureBuilder: def __init__(self, name: str, description: str = None, access_levels: List[str] = None, monthly_quota: int = 1, activated: bool = False, feature_id: str = None, feature_path: str = None ): self.app_id = envs.APP_ID self.name = name self.description = description self.access_levels = access_levels self.monthly_quota = monthly_quota self.activated = activated self.feature_id = feature_id self.feature_path = feature_path self.get_details() def get_details(self): # ! There can't be 2 features with the same `name` # `decorators` will be applied on the route created # `access_levels` will be verified with auth # Ex: # access_levels = ['admin'] will check # `shared_tenant` table `access_level` column for `admin` value # or if user is the tenant owner if self.feature_id is None: self.feature_id = self.name.lower().replace(" ", "_") + '_feature' if self.feature_path is None: self.feature_path = '/' + \ self.feature_id.replace("_", "-").replace('-feature', "") return { 'app_id': self.app_id, 'name': self.name, 'description': self.description, 'access_levels': self.access_levels, 'monthly_quota': self.monthly_quota, 'activated': self.activated, 'feature_id': self.feature_id, 'feature_path': self.feature_path } def feature_is_activated(self, tenant_id: str): feature_activated = mongodata.document_count( match={ "tenant_id": tenant_id, "feature_id": self.feature_id, "activated": True }, collection=envs.MONGO_COLLECTION_FEATURES_NAME ) return True if feature_activated else False def update_quota(self, flask_request: Request, units: int = 1): tenant_id = flask_request.headers.get("TenantId") if not self.feature_is_activated(tenant_id): return "Feature is not activated. Can't update quota", 400 q = Quota( tenant_id=tenant_id, auth_token=flask_request.headers.get("Authorization"), units=self.monthly_quota, uploader_id=self.feature_id ) res, status_code = q.check_quota(units) if status_code == 200: return q.update_quota(units) return res, status_code def get_status(self, flask_request: Request): tenant_id = flask_request.headers.get("TenantId") results = mongodata.fetch( match=({'tenant_id': tenant_id, "name": self.name}, {"_id": 0, "feature_path": 0}), collection=envs.MONGO_COLLECTION_FEATURES_NAME ) if not results: return {}, 200 quotas = mongodata.fetch( match=({'tenant_id': tenant_id, "uploader_id": self.feature_id}, { "_id": 0, "feature_path": 0}), collection=envs.MONGO_COLLECTION_UTILIZATION_NAME ) if quotas: results[0]['monthly_quota_consumed'] = quotas[0]['monthly_quota_consumed'] results[0]['quota_reset_date'] = quotas[0]['quota_reset_date'] return results[0], 200 def set_status(self, tenant_id: str, status: bool): self.activated = status feature_details = self.get_details() feature_details['tenant_id'] = tenant_id updated = mongodata.update( schema=FeaturesSchema, match={'tenant_id': tenant_id, 'name': self.name}, new_data=feature_details, collection=envs.MONGO_COLLECTION_FEATURES_NAME ) return updated def update_status(self, flask_request: Request): status = flask_request.json['activated'] tenant_id = flask_request.headers.get("TenantId") resp = f"Feature {'activated' if status else 'deactivated'}", 200 if len(self.access_levels) == 0: self.set_status(tenant_id, status) return resp if current_user_has_access_level(flask_request, self.access_levels): self.set_status(tenant_id, status) return resp return "Not enough rights to activate/deactivate feature", 401
Methods
def feature_is_activated(self, tenant_id: str)
-
Expand source code
def feature_is_activated(self, tenant_id: str): feature_activated = mongodata.document_count( match={ "tenant_id": tenant_id, "feature_id": self.feature_id, "activated": True }, collection=envs.MONGO_COLLECTION_FEATURES_NAME ) return True if feature_activated else False
def get_details(self)
-
Expand source code
def get_details(self): # ! There can't be 2 features with the same `name` # `decorators` will be applied on the route created # `access_levels` will be verified with auth # Ex: # access_levels = ['admin'] will check # `shared_tenant` table `access_level` column for `admin` value # or if user is the tenant owner if self.feature_id is None: self.feature_id = self.name.lower().replace(" ", "_") + '_feature' if self.feature_path is None: self.feature_path = '/' + \ self.feature_id.replace("_", "-").replace('-feature', "") return { 'app_id': self.app_id, 'name': self.name, 'description': self.description, 'access_levels': self.access_levels, 'monthly_quota': self.monthly_quota, 'activated': self.activated, 'feature_id': self.feature_id, 'feature_path': self.feature_path }
def get_status(self, flask_request: flask.wrappers.Request)
-
Expand source code
def get_status(self, flask_request: Request): tenant_id = flask_request.headers.get("TenantId") results = mongodata.fetch( match=({'tenant_id': tenant_id, "name": self.name}, {"_id": 0, "feature_path": 0}), collection=envs.MONGO_COLLECTION_FEATURES_NAME ) if not results: return {}, 200 quotas = mongodata.fetch( match=({'tenant_id': tenant_id, "uploader_id": self.feature_id}, { "_id": 0, "feature_path": 0}), collection=envs.MONGO_COLLECTION_UTILIZATION_NAME ) if quotas: results[0]['monthly_quota_consumed'] = quotas[0]['monthly_quota_consumed'] results[0]['quota_reset_date'] = quotas[0]['quota_reset_date'] return results[0], 200
def set_status(self, tenant_id: str, status: bool)
-
Expand source code
def set_status(self, tenant_id: str, status: bool): self.activated = status feature_details = self.get_details() feature_details['tenant_id'] = tenant_id updated = mongodata.update( schema=FeaturesSchema, match={'tenant_id': tenant_id, 'name': self.name}, new_data=feature_details, collection=envs.MONGO_COLLECTION_FEATURES_NAME ) return updated
def update_quota(self, flask_request: flask.wrappers.Request, units: int = 1)
-
Expand source code
def update_quota(self, flask_request: Request, units: int = 1): tenant_id = flask_request.headers.get("TenantId") if not self.feature_is_activated(tenant_id): return "Feature is not activated. Can't update quota", 400 q = Quota( tenant_id=tenant_id, auth_token=flask_request.headers.get("Authorization"), units=self.monthly_quota, uploader_id=self.feature_id ) res, status_code = q.check_quota(units) if status_code == 200: return q.update_quota(units) return res, status_code
def update_status(self, flask_request: flask.wrappers.Request)
-
Expand source code
def update_status(self, flask_request: Request): status = flask_request.json['activated'] tenant_id = flask_request.headers.get("TenantId") resp = f"Feature {'activated' if status else 'deactivated'}", 200 if len(self.access_levels) == 0: self.set_status(tenant_id, status) return resp if current_user_has_access_level(flask_request, self.access_levels): self.set_status(tenant_id, status) return resp return "Not enough rights to activate/deactivate feature", 401