Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 16 additions & 17 deletions service/models.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
######################################################################
# Copyright 2016, 2023 John Rofrano. All Rights Reserved.
# Copyright 2016, 2026 John Rofrano. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the 'License');
# you may not use this file except in compliance with the License.
Expand All @@ -13,10 +13,13 @@
# See the License for the specific language governing permissions and
# limitations under the License.
######################################################################
# cSpell: disable=flushall
"""
Counter Model
"""

import logging
from typing import Optional, Self
from redis.exceptions import ConnectionError as RedisConnectionError
from service import redis

Expand All @@ -38,7 +41,7 @@ class Counter:
This follows the same standards as SQLAlchemy URIs
"""

def __init__(self, name: str = "hits", value: int = None):
def __init__(self, name: str = "hits", value: Optional[int] = None) -> None:
"""Constructor"""
self.name = name
if not value:
Expand All @@ -47,49 +50,45 @@ def __init__(self, name: str = "hits", value: int = None):
self.value = value

@property
def value(self):
def value(self) -> int:
"""Returns the current value of the counter"""
return int(redis.get(self.name))

@value.setter
def value(self, value):
def value(self, value: int) -> None:
"""Sets the value of the counter"""
redis.set(self.name, value)

@value.deleter
def value(self):
def value(self) -> None:
"""Removes the counter fom the database"""
redis.delete(self.name)

def increment(self):
def increment(self) -> int:
"""Increments the current value of the counter by 1"""
return redis.incr(self.name)

def serialize(self):
def serialize(self) -> dict[str, str | int]:
"""Converts a counter into a dictionary"""
return {
"name": self.name,
"counter": int(redis.get(self.name))
}
return {"name": self.name, "counter": int(redis.get(self.name))}

######################################################################
# F I N D E R M E T H O D S
######################################################################

@classmethod
def all(cls):
def all(cls) -> list:
"""Returns all of the counters"""
try:
counters = [
{"name": key, "counter": int(redis.get(key))}
for key in redis.keys("*")
{"name": key, "counter": int(redis.get(key))} for key in redis.keys("*")
]
except Exception as err:
raise DatabaseConnectionError(err) from err
return counters

@classmethod
def find(cls, name):
def find(cls, name) -> Optional[Self]:
"""Finds a counter with the name or returns None"""
counter = None
try:
Expand All @@ -98,10 +97,10 @@ def find(cls, name):
counter = Counter(name, count)
except Exception as err:
raise DatabaseConnectionError(err) from err
return counter
return counter # type: ignore

@classmethod
def remove_all(cls):
def remove_all(cls) -> None:
"""Removes all of the keys in the database"""
try:
redis.flushall()
Expand Down
50 changes: 26 additions & 24 deletions service/routes.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
######################################################################
# Copyright 2016, 2023 John J. Rofrano. All Rights Reserved.
# Copyright 2016, 2026 John J. Rofrano. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
Expand All @@ -14,26 +14,25 @@
# limitations under the License.
######################################################################

# pylint: disable=cyclic-import
"""
Module for Hit Counter Service Routes
"""

import os
from flask import jsonify, abort, url_for
from os import getenv
from flask import jsonify, abort, url_for, Response
from flask import current_app as app
from service.common import status # HTTP Status Codes
from service.models import Counter, DatabaseConnectionError

DEBUG = os.getenv("DEBUG", "False") == "True"
PORT = os.getenv("PORT", "8080")
DEBUG = getenv("DEBUG", "False") == "True"
PORT = getenv("PORT", "8080")


############################################################
# Health Endpoint
############################################################
@app.route("/health")
def health():
def health() -> tuple[dict, int]:
"""Health Status"""
return {"status": "OK"}, status.HTTP_200_OK

Expand All @@ -42,22 +41,25 @@ def health():
# Index page
############################################################
@app.route("/")
def index():
def index() -> tuple[Response, int]:
"""Root URL"""
app.logger.info("Request for Base URL")
return jsonify(
status=status.HTTP_200_OK,
message="Hit Counter Service",
version="1.0.0",
url=url_for("list_counters", _external=True),
return (
jsonify(
status=status.HTTP_200_OK,
message="Hit Counter Service",
version="1.0.0",
url=url_for("list_counters", _external=True),
),
status.HTTP_200_OK,
)


############################################################
# List counters
############################################################
@app.route("/counters", methods=["GET"])
def list_counters():
def list_counters() -> tuple[list[Counter], int]:
"""List counters"""
app.logger.info("Request to list all counters...")
counters = []
Expand All @@ -66,14 +68,14 @@ def list_counters():
except DatabaseConnectionError as err:
abort(status.HTTP_503_SERVICE_UNAVAILABLE, err)

return jsonify(counters)
return counters, status.HTTP_200_OK


############################################################
# Read counters
############################################################
@app.route("/counters/<name>", methods=["GET"])
def read_counters(name):
def read_counters(name: str) -> tuple[dict, int]:
"""Read a counter"""
app.logger.info("Request to Read counter: %s...", name)

Expand All @@ -86,28 +88,28 @@ def read_counters(name):
abort(status.HTTP_404_NOT_FOUND, f"Counter {name} does not exist")

app.logger.info("Returning: %d...", counter.value)
return jsonify(counter.serialize())
return counter.serialize(), status.HTTP_200_OK


############################################################
# Create counter
############################################################
@app.route("/counters/<name>", methods=["POST"])
def create_counters(name):
def create_counters(name: str) -> tuple[dict, int, dict]:
"""Create a counter"""
app.logger.info("Request to Create counter...")
try:
counter = Counter.find(name)
if counter is not None:
return jsonify(code=409, error="Counter already exists"), 409
abort(status.HTTP_409_CONFLICT, f"Counter '{name}' already exists")

counter = Counter(name)
except DatabaseConnectionError as err:
abort(status.HTTP_503_SERVICE_UNAVAILABLE, err)

location_url = url_for("read_counters", name=name, _external=True)
return (
jsonify(counter.serialize()),
counter.serialize(),
status.HTTP_201_CREATED,
{"Location": location_url},
)
Expand All @@ -117,7 +119,7 @@ def create_counters(name):
# Update counters
############################################################
@app.route("/counters/<name>", methods=["PUT"])
def update_counters(name):
def update_counters(name: str) -> tuple[Response, int]:
"""Update a counter"""
app.logger.info("Request to Update counter...")
try:
Expand All @@ -129,14 +131,14 @@ def update_counters(name):
except DatabaseConnectionError as err:
abort(status.HTTP_503_SERVICE_UNAVAILABLE, err)

return jsonify(name=name, counter=count)
return jsonify(name=name, counter=count), status.HTTP_200_OK


############################################################
# Delete counters
############################################################
@app.route("/counters/<name>", methods=["DELETE"])
def delete_counters(name):
def delete_counters(name: str) -> tuple[dict, int]:
"""Delete a counter"""
app.logger.info("Request to Delete counter...")
try:
Expand All @@ -146,4 +148,4 @@ def delete_counters(name):
except DatabaseConnectionError as err:
abort(status.HTTP_503_SERVICE_UNAVAILABLE, err)

return "", status.HTTP_204_NO_CONTENT
return {}, status.HTTP_204_NO_CONTENT
Loading