[ADD] - Added social media user profile get and upsert microservice with nosql database integration

This commit is contained in:
PurvaG1700
2025-02-17 11:03:02 -08:00
parent 67bf966a6a
commit 12de833f66
12 changed files with 186 additions and 3 deletions

0
app/cache/__init__.py vendored Normal file
View File

104
app/cache/cache.py vendored Normal file
View File

@@ -0,0 +1,104 @@
from abc import ABC, abstractmethod
# implements a simple string k-v store, objects should be serialized before putting into the cache
class Cache(ABC):
@abstractmethod
def __init__(self, limit: int):
"""Constructor taking in the cache size limit as number of entries"""
pass
@abstractmethod
def get(self, key: str) -> str:
"""Get the value corresponding to key or returns None if there was a cache miss"""
pass
@abstractmethod
def put(self, key: str, val: str) -> bool:
"""Set the value corresponding to key and returns True if an eviction was made"""
pass
@abstractmethod
def invalidate(self, key: str) -> bool:
"""Mark cache item as invalid and returns True if the element was found and invalidated"""
pass
from collections import OrderedDict
# the baseline cache using Direct Mapping, LRU eviction, and no prefetching
class BaselineCache(Cache):
limit = None
cache = None
def __init__(self, limit: int):
super()
self.limit = limit
self.cache = OrderedDict()
def __eq__(self, other):
return self.cache == other
def __len__(self):
return len(self.cache)
def get(self, key: str) -> str:
if key in self.cache:
self.cache.move_to_end(key)
return self.cache[key]
else:
return None
def put(self, key: str, val: str) -> bool:
# LRU evict
evict = False
if len(self.cache) >= self.limit:
self.cache.popitem(last = False)
evict = True
self.cache[key] = val
# no need for this since this op appends key-val by default
# self.cache.move_to_end(key)
return evict
def invalidate(self, key: str) -> bool:
# basic delete invalidation, no (p)refetching
if key in self.cache:
del self.cache[key]
return True
else:
return False
if __name__ == "__main__": # basic testing, should never be called when importing
cache = BaselineCache(10)
for i in range(10):
assert cache.put(str(i), str(i+1)) == False
assert len(cache) == 10
assert cache == OrderedDict({'0': '1', '1': '2', '2': '3', '3': '4', '4': '5', '5': '6', '6': '7', '7': '8', '8': '9', '9': '10'})
assert cache.get("5") == "6"
assert cache.get("8") == "9"
assert cache.get("0") == "1"
assert len(cache) == 10
assert cache == OrderedDict({'1': '2', '2': '3', '3': '4', '4': '5', '6': '7', '7': '8', '9': '10', '5': '6', '8': '9', '0': '1'})
assert cache.get("a") == None
assert cache.get("b") == None
assert cache.get("c") == None
assert cache.put("a", "b") == True
assert cache.put("b", "c") == True
assert cache.put("c", "d") == True
assert len(cache) == 10
assert cache == OrderedDict({'4': '5', '6': '7', '7': '8', '9': '10', '5': '6', '8': '9', '0': '1', 'a': 'b', 'b' : 'c', 'c': 'd'})
assert cache.get("c") == "d"
assert cache.get("b") == "c"
assert cache.get("a") == "b"
assert len(cache) == 10
assert cache == OrderedDict({'4': '5', '6': '7', '7': '8', '9': '10', '5': '6', '8': '9', '0': '1', 'c': 'd', 'b' : 'c', 'a': 'b'})

28
app/cache/prefetch_cache.py vendored Normal file
View File

@@ -0,0 +1,28 @@
from .cache import BaselineCache
class PrefetchCache(BaselineCache):
key_relations = None
def __init__(self):
super()
self.key_relations = dict()
def put(self, key: str, val: str) -> bool:
# LRU evict
evict = False
if len(self.cache) >= self.limit:
self.cache.popitem(last = False)
evict = True
self.cache[key] = val
self.prefetch(key, val)
return evict
def prefetch(self, key: str, val: str) -> bool:
if len(self.cache) >= self.limit and key in self.key_relations:
self.cache[self.key_relations[key][0]] = self.key_relations[key][1]
return True
return False
def set_relations(self):
return

94
app/cache/tiered_cache.py vendored Normal file
View File

@@ -0,0 +1,94 @@
from .cache import BaselineCache
from collections import OrderedDict
import os
class TieredCache(BaselineCache):
l2_limit = None
l2_map = None
def __init__(self, limit, l2_limit = 100):
super().__init__(limit)
self.l2_limit = l2_limit
self.l2_map = OrderedDict()
def get(self, key):
# first look in the l1 cache
s = super().get(key)
if s != None:
return s
else: # on a miss, check the l2 cache mapping
if key in self.l2_map: # if it is in l2 cache (disk), open the file and return the values
f = open(self.l2_map[key], "r")
v = f.read()
f.close()
return v
else: # otherwise its a cache miss and return None
return None
def put(self, key, val):
evict = False
if len(self.cache) >= self.limit:
if len(self.l2_map) >= self.l2_limit:
self.l2_map.popitem(last = False)
evict = True
k,v = self.cache.popitem(last = False)
path = f"tiered_cache/{k}"
self.l2_map[k] = path
f = open(path, "w+")
f.write(v)
f.close()
self.cache[key] = val
return evict
def invalidate(self, key: str) -> bool:
# basic delete invalidation, no (p)refetching
if key in self.cache:
del self.cache[key]
return True
elif key in self.l2_map:
os.remove(self.l2_map[key]) # this is so sketchy
del self.l2_map[key]
return True
else:
return False
if __name__ == "__main__": # basic testing, should never be called when importing
cache = TieredCache(10)
for i in range(10):
assert cache.put(str(i), str(i+1)) == False
assert len(cache) == 10
assert cache == OrderedDict({'0': '1', '1': '2', '2': '3', '3': '4', '4': '5', '5': '6', '6': '7', '7': '8', '8': '9', '9': '10'})
assert cache.get("5") == "6"
assert cache.get("8") == "9"
assert cache.get("0") == "1"
assert len(cache) == 10
assert cache == OrderedDict({'1': '2', '2': '3', '3': '4', '4': '5', '6': '7', '7': '8', '9': '10', '5': '6', '8': '9', '0': '1'})
assert cache.get("a") == None
assert cache.get("b") == None
assert cache.get("c") == None
assert cache.put("a", "b") == False
assert cache.put("b", "c") == False
assert cache.put("c", "d") == False
assert len(cache) == 10
assert cache == OrderedDict({'4': '5', '6': '7', '7': '8', '9': '10', '5': '6', '8': '9', '0': '1', 'a': 'b', 'b' : 'c', 'c': 'd'})
assert cache.get("c") == "d"
assert cache.get("b") == "c"
assert cache.get("a") == "b"
assert cache.get("1") == "2"
assert cache.get("2") == "3"
assert cache.get("3") == "4"
assert len(cache) == 10
assert cache == OrderedDict({'4': '5', '6': '7', '7': '8', '9': '10', '5': '6', '8': '9', '0': '1', 'c': 'd', 'b' : 'c', 'a': 'b'})

15
app/config.py Normal file
View File

@@ -0,0 +1,15 @@
import os
import yaml
CONFIG_FILE = "config.yaml"
def load_config():
with open(CONFIG_FILE, "r") as f:
return yaml.safe_load(f)
config = load_config()
# Read from environment variable or fallback to YAML value
CACHE_STRATEGY = os.getenv("CACHE_STRATEGY", config.get("cache_strategy", "Baseline"))
CACHE_LIMIT = config.get("cache_limit", 10)
L2_CACHE_LIMIT = config.get("l2_cache_limit", 100)

3
app/config.yaml Normal file
View File

@@ -0,0 +1,3 @@
cache_strategy: "Baseline" # Change this to "Prefetch" or "Tiered"
cache_limit: 10
l2_cache_limit: 100

29
app/database.py Normal file
View File

@@ -0,0 +1,29 @@
from tinydb import TinyDB, Query
# Initialize TinyDB as a NoSQL key-value store
DB_FILE = "database.json"
db = TinyDB(DB_FILE)
User = Query()
def get_user_profile(user_id):
"""Fetch user profile from TinyDB"""
result = db.search(User.user_id == user_id)
return result[0] if result else None
def update_user_profile(user_id, name, followers, bio, posts):
"""Update user profile in TinyDB"""
db.upsert({"user_id": user_id, "name": name, "followers": followers, "bio": bio, "posts": posts}, User.user_id == user_id)
def init_db():
"""Ensure TinyDB is initialized before FastAPI starts and prepopulate some data"""
global db
db = TinyDB(DB_FILE) # Reload TinyDB if needed
# Prepopulate database with some sample users if empty
if len(db) == 0:
db.insert_multiple([
{"user_id": "1", "name": "Alice", "followers": 100, "bio": "Love coding!", "posts": "Hello, world!"},
{"user_id": "2", "name": "Bob", "followers": 200, "bio": "Tech enthusiast", "posts": "AI is amazing!"},
{"user_id": "3", "name": "Charlie", "followers": 50, "bio": "Blogger", "posts": "Check out my latest post!"}
])

39
app/main.py Normal file
View File

@@ -0,0 +1,39 @@
from fastapi import FastAPI, HTTPException
from database import get_user_profile, update_user_profile
from cache.cache import BaselineCache
from cache.prefetch_cache import PrefetchCache
from cache.tiered_cache import TieredCache
from config import CACHE_STRATEGY, CACHE_LIMIT, L2_CACHE_LIMIT
app = FastAPI()
# Initialize cache based on strategy from config.yaml or environment variable
if CACHE_STRATEGY == "Baseline":
cache = BaselineCache(limit=CACHE_LIMIT)
elif CACHE_STRATEGY == "Prefetch":
cache = PrefetchCache()
elif CACHE_STRATEGY == "Tiered":
cache = TieredCache(limit=CACHE_LIMIT, l2_limit=L2_CACHE_LIMIT)
else:
raise ValueError(f"Invalid CACHE_STRATEGY: {CACHE_STRATEGY}")
@app.get("/user/{user_id}")
def fetch_user_profile(user_id: str):
"""Fetch user profile with caching"""
cached_profile = cache.get(user_id)
if cached_profile:
return {"user_id": user_id, "profile": cached_profile, "source": "cache"}
profile = get_user_profile(user_id)
if profile is None:
raise HTTPException(status_code=404, detail="User not found")
cache.put(user_id, profile) # Store in cache
return {"user_id": user_id, "profile": profile, "source": "database"}
@app.post("/update_user/")
def modify_user_profile(user_id: str, name: str, followers: int, bio: str, posts: str):
"""Update user profile and refresh cache"""
update_user_profile(user_id, name, followers, bio, posts)
cache.invalidate(user_id) # Invalidate old cache
return {"message": "User profile updated successfully"}

12
app/run.py Normal file
View File

@@ -0,0 +1,12 @@
import os
import uvicorn
from database import init_db # Ensure database initializes before starting FastAPI
os.environ["PYTHONDONTWRITEBYTECODE"] = "1"
if __name__ == "__main__":
# Initialize TinyDB (NoSQL) before FastAPI starts
init_db()
# Start the FastAPI server with custom options
uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True, workers=2)