Monday, May 30, 2022

RedisJSON - CRUD Ops

Summary

I cover a shopping cart application use case in this post.  I implement a full CRUD set for the users, products, and carts using the RedisJSON module.

Architecture

The Server-side is implemented as an Express.js app with the node-redis module.  The test client is also Node-based with node-fetch used for the HTTP client and a random data generator I concocted with the uniqueNamesGenerator module.


Code Snippets

Random Data Generation

  1. static generateUser() {
  2. return {
  3. "userID": uuidv4(),
  4. "lastName": RandomData.#getLastName(),
  5. "firstName": RandomData.#getFirstName(),
  6. "street": RandomData.#getStreet(),
  7. "city": RandomData.#getCity(),
  8. "state": RandomData.#getState(),
  9. "zip": RandomData.#getZip()
  10. };
  11. };
  12. static #getFirstName() {
  13. return uniqueNamesGenerator({
  14. dictionaries:[names],
  15. length: 1
  16. });
  17. };
  18.  
  19. static #getLastName() {
  20. return uniqueNamesGenerator({
  21. dictionaries: [adjectives],
  22. length: 1,
  23. style: 'capital'
  24. });
  25. };


Create User - Client-side

  1. async function createUser(dbType, user) {
  2. const response = await fetch(`${SERVER.url}/${dbType}/user`, {
  3. method: 'POST',
  4. body: JSON.stringify(user),
  5. headers: {
  6. 'Content-Type': 'application/json',
  7. 'Authorization': AUTH
  8. }
  9. });
  10. return await response.json();
  11. };
  12.  
  13. const user = RandomData.generateUser();
  14. res = await createUser('redis', user);


Update Cart - Server-side

  1. app.patch('/:dbType/cart/:cartID', async (req, res) => {
  2. switch (req.params.dbType) {
  3. case 'redis':
  4. try {
  5. var client = await redisConnect();
  6. const updatedItem = req.body;
  7. const items = await client.json.get(`cart:${req.params.cartID}`, {path:'.items'});
  8. const newItems = [];
  9. let found = false
  10. for (let item of items) {
  11. if (updatedItem.sku == item.sku) {
  12. found = true;
  13. if (updatedItem.quantity == 0) {
  14. continue;
  15. }
  16. else {
  17. newItems.push(updatedItem)
  18. }
  19. break;
  20. }
  21. else {
  22. newItems.push(item);
  23. }
  24. }
  25. if (!found) {
  26. newItems.push(updatedItem)
  27. }
  28. const val = await client.json.set(`cart:${req.params.cartID}`, `.items`, newItems);
  29.  
  30. if (val == 'OK') {
  31. console.log(`200: Cart ${req.params.cartID} updated`);
  32. res.status(200).json({'cartID': req.params.cartID});
  33. }
  34. else {
  35. throw new Error(`Cart ${req.params.sku} not fully updated`);
  36. }
  37. }
  38. catch (err) {
  39. console.error(`400: ${err.message}`);
  40. res.status(400).json({error: err.message});
  41. }
  42. finally {
  43. await client.quit();
  44. };
  45. break;
  46. default:
  47. const msg = 'Unknown DB Type';
  48. console.error(`400: ${msg}`);
  49. res.status(400).json({error: msg});
  50. break;
  51. };
  52. });


Source


Copyright ©1993-2024 Joey E Whelan, All rights reserved.

Saturday, May 21, 2022

Azure Container Apps

Summary

This is a continuation of a previous post on proxying a SOAP API to REST.  In this post, I'll deploy the containerized proxy to Azure Container Apps and front end it with Azure API Management (APIM).

Architecture



Code

Proxy App

I modified the Python FastAPI app slightly to serve up an OpenAPI file.  That file is used by APIM during provisioning.

  1. from fastapi import FastAPI, HTTPException
  2. from fastapi.responses import FileResponse
  3. from zeep import Client
  4. import logging
  5.  
  6. logging.getLogger('zeep').setLevel(logging.ERROR)
  7. client = Client('https://www.w3schools.com/xml/tempconvert.asmx?wsdl')
  8. app = FastAPI()
  9.  
  10. @app.get("/openapi.yml")
  11. async def openapi():
  12. return FileResponse("openapi.yml")
  13.  
  14. @app.get("/CelsiusToFahrenheit")
  15. async def celsiusToFahrenheit(temp: int):
  16. try:
  17. soapResponse = client.service.CelsiusToFahrenheit(temp)
  18. fahrenheit = int(round(float(soapResponse),0))
  19. except:
  20. raise HTTPException(status_code=400, detail="SOAP request error")
  21. else:
  22. return {"temp": fahrenheit}
  23.  
  24.  
  25. @app.get("/FahrenheitToCelsius")
  26. async def fahrenheitToCelsius(temp: int):
  27. try:
  28. soapResponse = client.service.FahrenheitToCelsius(temp)
  29. celsius = int(round(float(soapResponse),0))
  30. except:
  31. raise HTTPException(status_code=400, detail="SOAP request error")
  32. else:
  33. return {"temp": celsius}

OpenAPI Spec


  1. swagger: '2.0'
  2. info:
  3. title: apiproxy
  4. description: REST to SOAP proxy
  5. version: 1.0.0
  6. schemes:
  7. - http
  8. produces:
  9. - application/json
  10. paths:
  11. /CelsiusToFahrenheit:
  12. get:
  13. summary: Convert celsius temp to fahrenheit
  14. parameters:
  15. - name: temp
  16. in: path
  17. required: true
  18. type: integer
  19. responses:
  20. '200':
  21. description: converted temp
  22. schema:
  23. type: object
  24. properties:
  25. temp:
  26. type: integer
  27. '400':
  28. description: General error
  29. /FahrenheitToCelsius:
  30. get:
  31. summary: Convert fahrenheit temp to celsius
  32. parameters:
  33. - name: temp
  34. in: path
  35. required: true
  36. type: integer
  37. responses:
  38. '200':
  39. description: converted temp
  40. schema:
  41. type: object
  42. properties:
  43. temp:
  44. type: integer
  45. '400':
  46. description: General error

Deployment


Create + Configure Azure Container Registry





Visual Studio Code - Build Image in Azure







Create + Configure Azure Container App






Execution

Deploy and Test Container App in APIM



Copyright ©1993-2024 Joey E Whelan, All rights reserved.

Sunday, May 15, 2022

RedisTimeSeries

Summary

I'll be covering an IoT data feed use case in this post.  That data feed originates from a Tempest weather station and is stored on a Redis instance as TimeSeries data.  The application is implemented as a container on Google Cloud Run.  The Redis TimeSeries data is visualized using Grafana.

Architecture



Application


ExpressJS REST API Server

Very simple server-side app to start and stop the data flow.
  1. app.post('/start', async (req, res) => {
  2. try {
  3. if (!tc) {
  4. tc = new TempestClient();
  5. await tc.start();
  6. res.status(201).json({'message': 'success'});
  7. }
  8. else {
  9. throw new Error('tempest client already instantiated');
  10. }
  11. }
  12. catch (err) {
  13. res.status(400).json({error: err.message})
  14. };
  15. });
  16.  
  17. app.post('/stop', async (req, res) => {
  18. try {
  19. if (tc) {
  20. await tc.stop();
  21. tc = null;
  22. res.status(201).json({'message': 'success'});
  23. }
  24. else {
  25. throw new Error('tempest client does not exist');
  26. }
  27. }
  28. catch (err) {
  29. res.status(400).json({error: err.message})
  30. };
  31. });

Tempest Client

Weatherflow provides a published REST and Websocket API.  In this case, I used their Websocket interface to provide a 3-second feed of wind data from the weather station.

  1. async start() {
  2. if (!this.ts && !this.ws) {
  3. this.ts = new TimeSeriesClient(redis.user, redis.password, redis.url);
  4. await this.ts.connect();
  5. this.ws = new WebSocket(`${tempest.url}?token=${tempest.password}`);
  6.  
  7. this.ws.on('open', () => {
  8. console.log('Websocket opened');
  9. this.ws.send(JSON.stringify(this.wsRequest));
  10. });
  11. this.ws.on('message', async (data) => {
  12. const obj = JSON.parse(data);
  13. if ("ob" in obj) {
  14. const time = Date.now()
  15. const speed = Number(obj.ob[1] * MS_TO_MPH).toFixed(1);
  16. const direction = obj.ob[2];
  17. console.log(`time: ${time} speed: ${speed} direction: ${direction}`);
  18. await this.ts.update(tempest.deviceId, time, speed, direction);
  19. }
  20. });
  21.  
  22. this.ws.on('close', async () => {
  23. console.log('Websocket closed')
  24. await this.ts.quit();
  25. this.ts = null;
  26. this.ws = null;
  27. });
  28.  
  29. this.ws.on('error', async (err) => {
  30. await this.ts.quit();
  31. this.ws.close();
  32. this.ts = null;
  33. this.ws = null;
  34. console.error('ws err: ' + err);
  35. });
  36. }
  37. }
  38.  
  39. async stop() {
  40. this.ws.close();
  41. }

Redis TimeSeries Client

I used the Node-Redis client to implement a function that performs a TimeSeries Add.  
  1. async update(deviceId, time, speed, direction) {
  2. await this.client.ts.add(`wind_direction:${deviceId}`, time, direction);
  3. await this.client.ts.add(`wind_speed:${deviceId}`, time, speed);
  4. }

Deployment

Dockerfile


  1. FROM node:18-slim
  2. WORKDIR /usr/src/app
  3. COPY package*.json ./
  4. RUN npm install
  5. COPY . .
  6. EXPOSE 8080
  7. CMD ["npm", "start"]

Redis Cloud + Insight




Google Cloud Code Integration with VS Code

The app container is deployed to Cloud Run using the Cloud Code tools.








Grafana Data Connection to Redis



Execution

CURL POST To Start Data Flow


curl -X POST https://redis-demo-y6pby4qk2a-uc.a.run.app/start -u yourUser:yourPassword

Redis Insight Real-time Feed


Cloud Run Console



Grafana Dashboard



Source


Copyright ©1993-2024 Joey E Whelan, All rights reserved.

Stable Marriage Problem - Python

 Summary

This short post covers a Python implementation of the Gale-Shapely algorithm for the Stable Marriage Problem (SMP).  Stable matching is a well-studied problem in multiple fields.  It has applications in nearly any two-sided market scenario.

Code Snippets

Preference Generator

Below is a Python function to generate a random set of preferences for two classes.  Those preferences are then subsequently used by Gale-Shapley to determine a stable matching.
  1. def generate_prefs(class1, class2):
  2. if len(class1) != len(class2):
  3. raise Exception("Invalid input: unequal list sizes")
  4.  
  5. prefs = {}
  6. for item in class1:
  7. random.shuffle(class2)
  8. prefs[item] = class2.copy()
  9. for item in class2:
  10. random.shuffle(class1)
  11. prefs[item] = class1.copy()
  12.  
  13. return dict(sorted(prefs.items()))

Gale-Shapley

  1. def gale_shapley(prefs, proposers):
  2. matches = []
  3. while len(proposers) > 0: #terminating condition - all proposers are matched
  4. proposer = proposers.pop(0) #Each round - proposer is popped from the free list
  5. proposee = prefs[proposer].pop(0) #Each round - the proposer's top preference is popped
  6. matchLen= len(matches)
  7. found = False
  8. for index in range(matchLen):
  9. match = matches[index]
  10. if proposee in match: #proposee is already matched
  11. found = True
  12. temp = match.copy()
  13. temp.remove(proposee)
  14. matchee = temp.pop()
  15. if prefs[proposee].index(proposer) < prefs[proposee].index(matchee): #proposer is a higher preference
  16. matches.remove(match) #remove old match
  17. matches.append([proposer, proposee]) #create new match with proposer
  18. proposers.append(matchee) #add the previous proposer to the free list of proposers
  19. else:
  20. proposers.append(proposer) #proposer wasn't a higher prefence, so gets put back on free list
  21. break
  22. else:
  23. continue
  24. if not found: #proposee was not previously matched so is automatically matched to proposer
  25. matches.append([proposer, proposee])
  26. else:
  27. continue
  28. return matches

Output

Below is a sample run with two three-member classes: (a1, a2, a3) and (b1, b2, b3).
Preferences
{'a1': ['b2', 'b3', 'b1'],
 'a2': ['b3', 'b2', 'b1'],
 'a3': ['b1', 'b2', 'b3'],
 'b1': ['a2', 'a1', 'a3'],
 'b2': ['a1', 'a3', 'a2'],
 'b3': ['a3', 'a1', 'a2']}

Matches
[['a3', 'b1'], ['a1', 'b2'], ['a2', 'b3']]

Source


Copyright ©1993-2024 Joey E Whelan, All rights reserved.