diff --git a/bin/coct-create-sr.py b/bin/coct-create-sr.py new file mode 100644 index 0000000..659f3a7 --- /dev/null +++ b/bin/coct-create-sr.py @@ -0,0 +1,121 @@ +import argparse +import base64 +import hashlib +import hmac +import logging +import pprint +import sys +import re + +import coct_sr_api_client +from coct_sr_api_client.api import auth_group_api, service_request_group_api +from coct_sr_api_client.models.request_attributes_schema import RequestAttributesSchema + +logger = logging.getLogger(__name__) + + +def validate_reference_number(value): + if not re.match(r'^9\d{9}$', value): + raise argparse.ArgumentTypeError('Reference number must be a 10-digit number starting with 9') + return str(value) + + +def validate_hex_string(value): + if not re.match(r'^[0-9a-fA-F]{32}$', value.replace("-", "")): + raise argparse.ArgumentTypeError('Invalid hex string: must be 32 characters long') + return str(value) + + +if __name__ == '__main__': + parser = argparse.ArgumentParser(description='Description of your program') + + # SR params + parser.add_argument('-t', '--type', type=int, required=True, + help='4 digit numerical type code') + parser.add_argument('-st', '--subtype', type=int, required=True, + help='4 digit numerical subtype code') + parser.add_argument('-m', '--message', type=str, required=True, + help='Message to pass into body of request') + parser.add_argument('-a', '--address', type=str, required=True, + help='Comma separated string description of location of issue') + parser.add_argument('-y', '--latitude', type=float, required=True, + help='Latitude of the issue') + parser.add_argument('-x', '--longitude', type=float, required=True, + help='Longitude of the issue') + parser.add_argument('-n', '--telephone', type=str, required=True, + help='Phone number of the reporter') + parser.add_argument('-e', '--email', type=str, required=True, + help='Email of the reporter') + parser.add_argument('-c', '--comm', type=str, required=True, choices=["EMAIL", "SMS", "NONE"], + help='Communication preference of the reporter. ' + 'EMAIL refers to email, SMS refers to SMS, and NONE refers to well, no preference') + parser.add_argument('-sn', '--street-number', type=str, required=True, + help='Street number of the service request.') + parser.add_argument('-str', '--street', type=str, required=True, + help='Street name of the service request.') + parser.add_argument('-s', '--suburb', type=str, required=True, + help='Suburb of the service request.') + # Non-SR params + parser.add_argument('-p', '--public-key', type=validate_hex_string, required=True, + help='Public key (32 character long UUID)') + parser.add_argument('-k', '--private-key', type=validate_hex_string, required=True, + help='Private key (32 character long UUID)') + parser.add_argument('-v', '--verbose', action='store_true', required=False, + help="Turn on verbose logging") + + args = parser.parse_args() + + logging.basicConfig(stream=sys.stdout, level=logging.DEBUG if args.verbose else logging.INFO, + format='%(asctime)s %(name)s.%(funcName)s %(levelname)s: %(message)s') + + # getting all script args and removing those that aren't SR parameters + sr_args = vars(args).copy() + for key in ["verbose", "public_key", "private_key"]: + del sr_args[key] + + request_attrs = RequestAttributesSchema(**sr_args) + logger.debug(f'Received arguments: {args.public_key=}, args.private_key="{"*" * len(args.private_key)}"') + logger.debug(f'Received sr_args: {request_attrs=}') + + configuration = coct_sr_api_client.Configuration( + host="https://qaeservices1.capetown.gov.za/coct/api" + ) + # Setting the service Identifier + configuration.api_key['serviceAuth'] = args.public_key.replace("-", "") + + with coct_sr_api_client.ApiClient(configuration) as api_client: + auth_api_instance = auth_group_api.AuthGroupApi(api_client) + logger.info("Logging into API") + login_response = auth_api_instance.zcur_guest_login_get_without_preload_content( + cookie=args.private_key.replace("-", "") + ) + + # Copying out the auth cookie, and setting it + # clunkier than I expected + set_cookie = login_response.headers['set-cookie'] + sap_sso_cookie, *_ = set_cookie.split(";") + configuration.api_key['cookieAuth'] = sap_sso_cookie + + logger.info("Logged into API") + + logger.info("Starting an API session") + # Setting the API session + session_response = auth_api_instance.zsreq_session_get() + configuration.api_key['sessionAuth'] = session_response.session_id + + # Creating the HMAC signature + message = bytes(request_attrs.to_json(), 'utf-8') + secret = bytes(args.private_key, 'utf-8') + signature = base64.b64encode(hmac.new(secret, message, digestmod=hashlib.sha256).digest()) + configuration.api_key['signatureAuth'] = signature + + # Creating the SR + sr_api_instance = service_request_group_api.ServiceRequestGroupApi(api_client) + create_sr_response = sr_api_instance.zsreq_sr_post(request_attrs) + logger.info(f"SR created successfully - {create_sr_response.reference_number}") + + # Looking up the current state of our newly created SR + sr_status_response = sr_api_instance.zsreq_sr_reference_no_get(reference_no=create_sr_response.reference_number) + logger.info(f"sr_status=\n{pprint.pformat((dict(sr_status_response)))}") + + logger.info("Ending API session") diff --git a/city-sr-api.json b/city-sr-api.json index ff10bff..03636c5 100644 --- a/city-sr-api.json +++ b/city-sr-api.json @@ -235,7 +235,8 @@ { "cookieAuth": [], "serviceAuth": [], - "sessionAuth": [] + "sessionAuth": [], + "signatureAuth": [] } ], "summary": "This operation is used to create a service request in the City’s system.", @@ -251,7 +252,14 @@ }, "responses": { "200": { - "description": "OK" + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Request_Attribute_Response_Schema" + } + } + } }, "500": { "description": "Error in retrieving all type codes and descriptions. (Internal)", @@ -293,11 +301,15 @@ "properties": { "name": { "type": "string", - "example": "Cat" + "description": "A description of the service or grouping represented by the code", + "example": "Animal carcass removal" }, "type": { "type": "integer", - "example": 1001 + "description": "A 4-character code identifying a specific grouping of services", + "example": 1001, + "minimum": 1000, + "maximum": 9999 } } }, @@ -311,15 +323,22 @@ "properties": { "name": { "type": "string", + "description": "A description of the service or grouping represented by the code", "example": "Cat" }, "type": { "type": "integer", - "example": 1001 + "description": "A 4-digit code identifying a specific grouping of services", + "example": 1001, + "minimum": 1000, + "maximum": 9999 }, "subtype": { "type": "integer", - "example": 1001 + "description": "A 4-digit code identifying an individual service", + "example": 1001, + "minimum": 1000, + "maximum": 9999 } } }, @@ -347,116 +366,141 @@ }, "Request_Attributes_Schema": { "required": [ - "account_number", - "address", - "category_1", - "category_2", - "comm", - "email", - "firstName", - "id", - "lastName", + "type", + "subtype", + "message", "latitude", "longitude", - "message", - "street", - "street_number", - "subtype", - "suburb", "telephone", - "type", - "username" + "comm", + "suburb", + "street", + "street_number" ], "type": "object", "properties": { - "id": { + "type": { "type": "integer", - "format": "int64", - "example": 1 - }, - "category_1": { - "type": "string", - "example": "Support COMPLAINTS" + "description": "A 4-digit code identifying a specific grouping of services. See path /zsreq/config/types for acceptable values.", + "example": 1001, + "minimum": 1000, + "maximum": 9999 }, - "category_2": { - "type": "string", - "example": "Street Lights - Single Light Out" - }, - "suburb": { - "type": "string", - "example": "ADRIAANSE" - }, - "email": { - "type": "string", - "example": "cdorsett3@loc.gov" + "subtype": { + "type": "integer", + "description": "A 4-digit code identifying an individual service. See path /zsreq/config/subtypes for acceptable values.", + "example": 1002, + "minimum": 1000, + "maximum": 9999 }, "message": { "type": "string", - "example": "This requests was captured by the Walking the Streets program." + "description": "The text describing the requested service as entered by the user.", + "example": "Please collect the whale carcass from Muizenberg beach" }, "address": { "type": "string", - "example": "08 Maple Wood Hill" + "description": "The comma-delimited address of the location of the service request.", + "example": "1 Beach Road, Muizenberg" + }, + "latitude": { + "type": "number", + "format": "float", + "description": "The latitude of the location of the issue (in decimal degrees)", + "minimum": -34.35, + "maximum": -33.47, + "example":-34.105420 + }, + "longitude": { + "type": "number", + "format": "float", + "description": "The longitude of the location of the issue (in decimal degrees)", + "minimum": 18.3, + "maximum": 19, + "example":18.474681 }, - "username": { + "email": { "type": "string", - "example": "" + "format": "email", + "description": "Valid email address (required if comm is “email”)", + "example": "concerned@beachwalker.co.za" }, - "account_number": { + "telephone": { "type": "string", - "example": "" + "pattern": "^(\\+27|0)\\d{9}$", + "description": "Valid local mobile telephone number", + "example": "+27811234756" }, "comm": { "type": "string", - "example": "Maple" + "description": "Communication preference. 'INT' is selects correspondence via email, 'SMS' is SMS text message via the telephone number.", + "enum": ["EMAIL", "SMS", "NONE"], + "example": "EMAIL" }, - "latitude": { + "suburb": { "type": "string", - "example": "-34.1080381288507" + "description": "The suburb name component of the location of the service request.", + "example": "Muizenberg" }, - "longitude": { + "street": { "type": "string", - "example": "18.47152590751648" - }, - "type": { - "type": "integer", - "example": 1005 + "description": "The street name component of the location of the service request.", + "example": "Beach Rd" }, - "subtype": { - "type": "integer", - "example": 1001 - }, - "telephone": { + "street_number": { "type": "string", - "example": "" + "description": "The street number component of the location of the service request.", + "example": "1" }, "firstName": { "type": "string", - "example": "Gwenora" + "pattern": "^.{0,40}$", + "description": "The first name of the contact person.", + "example": "Sipho" }, "lastName": { "type": "string", - "example": "Mellor" + "pattern": "^.{0,40}$", + "description": "The last name of the contact person.", + "example": "Daniels" + } + } + }, + "Request_Attribute_Response_Schema": { + "required": [ + "reference_number", + "category", + "message" + ], + "type": "object", + "properties": { + "reference_number": { + "type": "string", + "description": "The reference number of the service request", + "example": "000010011101" }, - "street": { + "category": { "type": "string", - "example": "Utah" + "description": "A description of the requested service category", + "example": "Animal carcass removal - Whale" }, - "street_number": { - "type": "integer", - "example": 76 + "message": { + "type": "string", + "description": "Text description including further details of the requested service and\nfeedback", + "example": "lease collect the whale carcass from Muizenberg beach before the wind direction\nchanges." } } }, "Request_Attributes_Lookup_Schema": { "required": [ - "description", - "message", "reference_number", - "status", + "type", "subtype", + "description", + "status", + "message", "time_created", - "type" + "date_created" ], "type": "object", "properties": { @@ -466,27 +510,44 @@ }, "type": { "type": "string", + "pattern": "^(\\d{4}|)$", + "description": "The four-digit code of the service group containing the selected service. See path /zsreq/config/types for possible values.", "example": "1001" }, "subtype": { "type": "string", + "pattern": "^(\\d{4}|)$", + "description": "The four-digit code of the specific service selected. See path /zsreq/config/subtypes for possible values.", "example": "1002" }, "description": { "type": "string", + "description": "A text description including further details of the requested service and feedback", "example": "Animal carcass removal - Whale" }, "status": { "type": "string", - "example": "Current state of work" + "description": "Current state of work", + "example": "Closed" }, "message": { "type": "string", - "example": "Please collect the whale carcass from Muizenberg beach" + "description": "A text description including further details of the requested service and feedback", + "example": " Please collect the whale carcass from Muizenberg beach before the wind direction changes. 28.11.2014 11:20:13 Gareth Dohne (SC_GD1) The carcass Please collect the whale carcass from Muizenberg beach has been successfully removed from the beach and the surrounding area cleaned up." }, "time_created": { "type": "string", - "example": "36539" + "format": "partial-time", + "pattern": "^([0-1]\\d|2[0-3])\\:[0-5]\\d\\:[0-5]\\d$", + "description": "The ISO8601 compliant partial time (HH:MM:SS) at which the request was created. SAST timezone is implied.", + "example": "10:08:59" + }, + "date_created": { + "type": "string", + "format": "city-custom-date", + "pattern": "^([0-2]\\d|3[0-1])\\.(0\\d|1[0-2])\\.\\d{4}$", + "description": "The date (NB not ISO8601 compliant, DD.MM.YYYY) on which the request was created. SAST timezone is implied.", + "example": "28.11.2014" } } } @@ -504,6 +565,12 @@ } }, "securitySchemes": { + "signatureAuth": { + "type": "apiKey", + "description": "Base 64 encoded, hexadecimal string of the HMAC SHA256 encoding of the message contents. See https://www.jokecamp.com/blog/examples-of-creating-base64-hashes-using-hmac-sha256-in-different-languages/ for examples of how to compute this for different languages.", + "name": "X-Signature", + "in": "header" + }, "cookieAuth": { "type": "apiKey", "description": "SAP Auth Cookie. See /coct/api/zcur-guest/login path for details on how to retrieve it.",