Skip to main content

Utilities & Enums Reference

info

Reference for the shared utilities, constants, enums, data classes, and request marshalling patterns used across the AquaGen API codebase. These are not feature-specific — they are building blocks used throughout routes, services, formatters, and batch processors.


DateTimeUtil (app/util/date_time_util.py)

warning

Never use datetime.now() directly or hardcode timezone offsets — always use DateTimeUtil for all timezone-aware date/time operations.

Timezone resolution — get_timezone(t_zone):
If t_zone is not passed, reads g.timezone set by the JWT user loader on every authenticated request. Falls back to 'Asia/Kolkata' for public routes where no user context exists.

MethodSignaturePurpose
convert_date_from_local_to_utc(value, input_format, t_zone)Converts a local datetime string to UTC. Default format: %Y-%m-%dT%H:%M:%SZ. Used by routes to convert DD/MM/YYYY request dates before Cosmos DB queries.
convert_date_from_utc_to_string(value, output_format, input_format, t_zone)Converts a UTC ISO string to a formatted local date string. Default output: %d/%m/%Y.
convert_date_time_from_utc_to_string(value, output_format, t_zone)Same as above but includes time. Default output: %d/%m/%Y %I:%M %p.
convert_date_from_utc_to_string_with_round_off(value, output_format, input_format, nearest_minute, t_zone)Converts UTC to local, then rounds to nearest nearest_minute seconds (default 300 = 5 min).
convert_date_from_utc_to_string_with_round_down(value, ..., nearest_minute, t_zone)Same but always rounds down (floor).
convert_date_from_utc_to_string_with_round_up(value, ..., nearest_minute, t_zone)Same but always rounds up (ceiling).
strptime(value, time_format, t_zone)datetime.strptime with automatic timezone attachment. Handles both aware and naive datetime strings via a try/catch fallback.
get_today_date(server, t_zone)Returns datetime.now() when server=True. When False, converts now to UTC in the industry timezone.
get_current_time(output_format, t_zone)Returns current local time as a formatted string.
get_n_dates(date, count)Returns a list of count dates counting back from date - 2 days. Used for quality week graphs.
get_dates_between_dates(start_date, end_date)Returns every calendar day between two datetime objects as DD/MM/YYYY strings (inclusive).
convert_seconds(seconds)Converts a seconds integer to a human-readable string: "X Days Y Hours Z Minutes".

Usage example:

from app.util.date_time_util import DateTimeUtil

date1_utc = DateTimeUtil().convert_date_from_local_to_utc(
f'{date1}T{shift_start}', '%d/%m/%YT%H:%M:%S'
)

Constants (app/constants/constants.py)

note

Constants is a class with only class-level attributes — no instantiation needed. Import and reference directly.

ConstantValueWhere used
max_daily_device_data_per_day288Maximum expected 5-minute readings per day (24h × 12)
daily_min_data_threshold284Minimum readings for a day to be considered "complete"
OFFLINE_THRESHOLD_HOUR15 minDevice considered offline in hour view
OFFLINE_THRESHOLD_DAY60 minDevice considered offline in day view
OFFLINE_THRESHOLD_MONTH1800 minDevice considered offline in month view
DEFAULT_USER_LIMIT5Default max users per industry
DEFAULT_SSO_USER_LIMIT10Default max SSO users per industry
DEFAULT_ALERTS_LIMIT10Default max alerts per query
RO_PLANT_EFFICIENCY_THRESHOLD50Below this % → RO plant is inefficient
RO_PLANT_SUDDEN_DROP_THRESHOLD15Drop greater than this % in one period → sudden drop alert
RO_PLANT_HEALTH_SCORE_POOR50Health score below this → "Poor"
RO_PLANT_HEALTH_SCORE_FAIR70Health score below this → "Fair"
MONTHLY_BATCH_PROCESS_EXCLUDED_INDUSTRY_IDSListIndustries skipped by the automated monthly batch shell script
TEST_INDUSTRY_IDSListDemo/test industries — excluded from production metrics
ADMIN_ALERT_TYPESListAlert types that are admin-only: device_status, no_flow_n_days, no_level_variation_n_days, water_balance_leakage
PUBLIC_ROUTESListExact paths that skip JWT auth entirely
PUBLIC_PREFIXESListPath prefixes that skip JWT auth (Swagger UI, virtual routes, batch trigger)
quality_si_unitsDictDisplay units for quality parameters: {"pH": "", "TDS": "ppm", "COD": "mg/L", ...}

Usage example:

from app.constants.constants import Constants

if elapsed_minutes > Constants.OFFLINE_THRESHOLD_DAY:
mark_offline()

Enums (app/enum/)

info

All string literals used for category types, report types, permissions, and statuses are defined as Python Enum classes. Always reference enum values rather than hardcoding strings.

StandardCategoryType — standard_category_type_enum.py

Maps sensor categories to their Cosmos DB string identifiers. Used in batch processing dispatch, formatter selection, and device data routing.

MemberValue
SOURCE_CATEGORY'SOURCE_CATEGORY'
ENERGY_CATEGORY'ENERGY_CATEGORY'
STOCK_CATEGORY'STOCK_CATEGORY'
GROUND_WATER_LEVEL'GROUND_WATER_LEVEL'
QUALITY_CATEGORY'QUALITY_CATEGORY'
VIRTUAL_CATEGORY'VIRTUAL_CATEGORY'

StandardCategoryType.all_categories() returns all values as a flat list.

CategoryType — category_type.py

Same categories but with camelCase member names — used when keying into current_user['industryData']['categories'].

MemberValue
sourceCategory'SOURCE_CATEGORY'
energyCategory'ENERGY_CATEGORY'
stockCategory'STOCK_CATEGORY'
groundWaterLevel'GROUND_WATER_LEVEL'
qualityCategory'QUALITY_CATEGORY'
rainWaterCategory'RAIN_WATER_CATEGORY'
virtualCategory'VIRTUAL_CATEGORY'
Why two category enums?

StandardCategoryType uses UPPER_CASE members matching Cosmos DB document values. CategoryType uses camelCase members matching the current_user dict keys. Use StandardCategoryType in batch processing and formatters; use CategoryType when accessing industryData.categories.

PatternType / PatternTypeV2 — pattern_type.py

Controls aggregation granularity in device data endpoints.

PatternType (v1 — GET /api/user/deviceData):

MemberValue
hour'HOUR'
date'DATE'
month'MONTH'
year'YEAR'

PatternTypeV2 (v2 — GET /api/user/deviceDataV2):

MemberValuegraph_key()
day'DAY''hourly'
month'MONTH''daily'
year'YEAR''monthly'
custom'CUSTOM''daily'

graph_key() returns the array field name used in the formatted response for that pattern — e.g. DAY view returns data in hourly[].

ReportType — report_type.py

MemberValue
DAILY'daily'
MONTHLY'monthly'
HOURLY'hourly'
GRANULAR'granular'
WEEKLY'weekly'
CUSTOM'custom'
SUMMARY'summary'
DAILY_RANGE'daily_range'
MONTHLY_RANGE'monthly_range'
AQUA_GPT'aqua_gpt'

ServiceType — service_type.py

Maps the service request parameter to a report class. Also provides standard_category_id() — returns the CategoryType member that corresponds to this service (e.g. ServiceType.LEVEL.standard_category_id()CategoryType.stockCategory).

ValueMaps to
waterFlowReport
rain_waterRainWaterReport
custom_waterCustomFlowReport
custom_withdrawalCustomWithdrawalReport
levelLevelReport
energyEnergyReport
borewellBorewellReport
qualityQualityReport
summarySummaryReport
consolidatedConsolidatedReport
ground_water_withdrawalGroundWaterWithdrawalReport
alertAlertReport
custom_levelCustomLevelReport
custom_qualityCustomQualityReport
custom_energyCustomEnergyReport
smart_citySmartCityReport
water_balanceWaterBalanceReport
executiveExecutiveReport

ReportFormat — report_format.py

MemberValueHelper methods
PDF'pdf'is_pdf()True, is_html()True
XLSX'xlsx'download_images()True
CSV'csv'
HTML'html'is_html()True
note

is_html() returns True for both PDF and HTML because PDF generation first renders the Jinja2 HTML template before passing it to xhtml2pdf.

Permission — permission.py

Controls feature access per user. Checked via @permission_required(Permission.X) decorator or permission_util.is_authorized(Permission.X).

ValueFeature
ACCOUNT_SETTINGSManage account settings
SUPER_USERSuper-user access
ALERTSView and manage alerts
LANDING_PAGEAccess dashboard landing page
MANUAL_NODE_ENTRYEnter data for virtual nodes manually
WATER_BALANCEWater balance graph and analysis
AQUAGPTAquaGPT AI assistant
EFFICIENCYRO efficiency tracking
UWIUrban Water Intelligence
RWIRecycled Water Intelligence
DAILY_SUMMARYDaily summary reports
ENERGY_ALERTEnergy-specific alerts
WATER_NEUTRALITYWater Neutrality Index
WATER_RATIOWater ratio standards
USERUser management
ADMINAdmin panel access
DISABLE_ENERGY_TOGGLE_BUTTONHide energy toggle in UI
WRIWater Reuse Intelligence

NodeTypes — node_types.py

Used in water balance graph construction to distinguish physical meters from computed aggregate nodes.

MemberValue
physicalNode'PHYSICAL_NODE'
virtualNode'VIRTUAL_NODE'

ResponseStatus — response_status.py

MemberValue
success'success'
microsoft'failed'

Utility Functions (app/util/)

number_util.py

FunctionPurpose
trunc(value, ndigits=2)Truncates (not rounds) a float to ndigits decimal places. Returns value unchanged if falsy. Used everywhere values are displayed to avoid floating-point rounding surprises.
is_number(data)Returns True if data is int, float, or None.
from app.util.number_util import trunc

trunc(12.3456, 2) # → 12.34 (truncated, not 12.35)
trunc(0, 2) # → 0
trunc(None, 2) # → None
Use trunc, not round

trunc truncates toward zero — it does not round. trunc(12.999, 2)12.99, not 13.00. Use this wherever sensor values are displayed to users.

si_unit_util.py

FunctionPurpose
get_si_unit(industry, category_id)Returns the display SI unit string for a category. Checks industry['meta']['siUnit'][category_id] first; falls back to CachedData.standardCategoriesMap[category_id]['siUnit']. STOCK_CATEGORY is remapped to SOURCE_CATEGORY before lookup.

permission_util.py

FunctionPurpose
is_authorized(permission)Checks current_user['permissions'] for the given Permission enum value or list of values. If a list is passed, all permissions must be present. Returns True/False.
from flask_restx import marshal
from app.util.permission_util import is_authorized
from app.enum.permission import Permission
from app.models.error_models import GenericErrorModel

if not is_authorized(Permission.WATER_BALANCE):
return marshal({'status': 'failed', 'message': 'Unauthorized'}, GenericErrorModel), 200

# Check multiple permissions — all must be present
if not is_authorized([Permission.ALERTS, Permission.SUPER_USER]):
return marshal({'status': 'failed', 'message': 'Unauthorized'}, GenericErrorModel), 200

filter_units_util.py

FunctionPurpose
get_units_for_category(current_user, category_type, exclude_virtual=True)Returns a deduplicated flat list of unit IDs for a given category_type string from current_user['industryData']['categories']. Excludes virtual units by default.
get_selected_units(data, unit_ids=None)Filters data['subCategories'] to only include units present in unit_ids. Returns data unchanged if unit_ids is None.
from app.util.filter_units_util import get_units_for_category
from app.enum.category_type import CategoryType

unit_ids = get_units_for_category(
current_user,
CategoryType.sourceCategory.value,
exclude_virtual=True
)

shift_and_unit_count_util.py

FunctionPurpose
get_shift_and_total_units(data, unit_ids=None)Returns a human-readable shift + unit count string: "06:00 AM to 06:00 AM | Total units : 12". Used in report headers and daily summary pages.

calculation_util.py

FunctionPurpose
level_calculation_w_percent(max_capacity, percent_remaining)Converts a percentage reading to an absolute level: round(maxCapacity × (percent / 100), 3). Used in stock and water balance level computations.

Data Classes (app/data_class/)

info

Data classes are typed Python @dataclass objects used to pass structured, validated arguments between the route layer and services. They replace raw dict passing and make service constructors self-documenting. Use them when a service takes many arguments, the same bundle crosses multiple layers, or optional fields need meaningful defaults.

ReportRequestDatadata_class/report.py

The primary DTO passed from the report route into ReportService and all report type classes.

@dataclass
class ReportRequestData:
industry_id: str
start_date: str
end_date: str
end_date_actual: str # may differ from end_date for monthly/yearly reports
report_type: ReportType
serviceType: ServiceType
report_format: ReportFormat
timezone: str
use_shift: bool
division_factor: int = 1000
unit_id: str = ''
unit_ids: List[str] = None
additional_data: object = None

DeviceDataV2Argsdata_class/device_data_v2_args.py

Passed from the v2 device data route into DeviceDataV2FormatterBuilder and all v2 formatter classes. Wraps both UTC and local date representations so formatters never recompute timezone conversions.

@dataclass
class Date:
utc: datetime
local: datetime

@dataclass
class DeviceDataV2Args:
date1: Date
categoryID: str
category: dict
divisionFactor: int
industryId: str
type: PatternTypeV2 # DAY / MONTH / YEAR / CUSTOM
dates: List[Date]
sdId: StandardCategoryType
date2: Date = None # comparison date (optional)
v2: bool = False

Other data classes follow the same pattern — see data_class/alert/, data_class/rainfall.py, and data_class/landingPage/ for further examples.


Request Marshalling & Parsing (app/models/)

info

Flask-RESTX uses two complementary mechanisms:

  • Request parsers (reqparse) — parse and validate incoming request arguments (headers, query params, POST form/JSON body)
  • Response models (Model + fields) — document and serialize outgoing responses in Swagger UI

Request Parsers (reqparse)

Request parsers extract and validate arguments from an incoming request. They are the Flask-RESTX equivalent of form validation.

from flask_restx import reqparse

parser = reqparse.RequestParser()
parser.add_argument('start_date', type=str, required=True, location='headers', help='Start date DD/MM/YYYY')
parser.add_argument('end_date', type=str, required=True, location='headers')
parser.add_argument('unit_id', type=str, required=False, location='headers', default='')
parser.add_argument('page', type=int, required=False, location='args', default=1)
location valueWhere Flask-RESTX looks
'headers'HTTP request headers
'args'URL query string (?key=value)
'json'JSON request body (Content-Type: application/json)
'form'Form-encoded body (Content-Type: application/x-www-form-urlencoded)
'files'Multipart file uploads

Parse inside a route handler:

def get(self):
args = parser.parse_args()
start_date = args['start_date']
unit_id = args.get('unit_id', '')
note

In AquaGen API, most parameters arrive as request headers (not query string or body). Always check an existing route to confirm the expected location before adding a new parser.

If a required=True argument is missing, Flask-RESTX automatically returns a 400 with a message from the help field — no manual checking needed.


POST Request Body (JSON)

For POST endpoints that receive a JSON body, use location='json':

post_parser = reqparse.RequestParser()
post_parser.add_argument('industry_id', type=str, required=True, location='json')
post_parser.add_argument('alert_type', type=str, required=True, location='json')
post_parser.add_argument('threshold', type=float, required=False, location='json', default=0.0)

Alternatively, document the expected body as a Model and use @namespace.expect():

AlertPayload = namespace.model('AlertPayload', {
'industry_id': fields.String(required=True),
'alert_type': fields.String(required=True),
'threshold': fields.Float(default=0.0),
})

@namespace.route('/alert')
class AlertRoute(Resource):
@namespace.expect(AlertPayload, validate=True) # validates body against model
def post(self):
data = request.json
industry_id = data['industry_id']
...

validate=True makes Flask-RESTX reject the request (400) if required fields are missing — and the model is displayed as the request body schema in Swagger.


Response Models (Model + fields)

Models document and serialize outgoing responses. Define them in app/models/, register them on the namespace, then attach via @namespace.response().

error_models.py

GenericErrorModel = Model('GenericErrorModel', {
'message': fields.String,
'status': fields.String,
})

success_models.py

GenericSuccessModel = Model('GenericSuccessModel', {
'message': fields.String,
'status': fields.String,
})

GenericSuccessDataModel = Model('GenericSuccessDataModel', {
'status': fields.String(),
'data': fields.Raw(), # untyped — use for complex/variable response shapes
})

Registering and using a model in a route

from flask_restx import Namespace, Resource, marshal
from app.models.my_feature_models import MyFeatureResponseModel

myFeatureNamespace = Namespace('My Feature', path='/myFeature')

# Register the model on the namespace for Swagger
myFeatureNamespace.add_model(MyFeatureResponseModel.name, MyFeatureResponseModel)

@myFeatureNamespace.route('')
class MyFeatureRoute(Resource):
@jwt_required()
@myFeatureNamespace.response(200, 'Success', model=MyFeatureResponseModel)
@myFeatureNamespace.response(1403, 'Missing Keys')
def get(self):
result = ...
return marshal({'status': 'success', 'data': result}, MyFeatureResponseModel), 200

Flask-RESTX field types

FieldUse case
fields.StringPlain string
fields.IntegerInteger number
fields.FloatFloat number
fields.BooleanBoolean
fields.RawUntyped — any JSON value. Use for variable-shape data (nested dicts, arrays)
fields.List(fields.Nested(Model))Typed array of nested model objects
fields.Nested(Model)Inline nested model
CustomDateFormatdd-mm-yyyy — defined in app/util/custom_flask_restx_fields.py
CustomTimeFormatHH:MM AM/PM — same file
CustomDateTimeFormatdd-mm-yyyy HH:MM AM/PM — same file

Custom field types (app/util/custom_flask_restx_fields.py)

The three custom fields extend fields.Raw and override format() to control serialization and the Swagger schema type hint:

class CustomDateFormat(fields.Raw):
__schema_type__ = "dd-mm-yyyy"
def format(self, value):
return value.strftime('%d-%m-%Y')

class CustomTimeFormat(fields.Raw):
__schema_type__ = "HH:MM AM/PM"
def format(self, value):
return value.strftime('%I:%M %p')
tip

Use these in models wherever a datetime object needs to be serialized to a display string rather than an ISO timestamp.

Marshalling with marshal()

marshal() is used directly in every route's return statement to serialize the response through the model. This is the standard pattern across AquaGen API:

from flask_restx import marshal
from app.models.error_models import GenericErrorModel
from app.models.success_models import GenericSuccessDataModel

# Success response
return marshal({'status': 'success', 'data': result}, GenericSuccessDataModel), 200

# Error response
return marshal({'message': 'Unauthorized', 'status': 'failed'}, GenericErrorModel), 401

Model nesting pattern

For complex responses, nest models rather than using fields.Raw — this gives Swagger accurate schema documentation:

AlertModel = Model('Alert', {
'id': fields.String,
'description': fields.Nested(AlertDescriptionModel)
})

AlertCategoryModel = Model('AlertCategory', {
'meta': fields.Nested(AlertMetaModel),
'alerts': fields.List(fields.Nested(AlertModel))
})
note

fields.Raw is appropriate only when the shape genuinely varies at runtime (e.g. data fields that differ per report type).


Further Reading