Let's Forge a Tiny Database! πŸ”©πŸ’Ύ

Welcome to your next adventure in building from scratch! This time, we're diving into the world of databases. At its core, a database is simply an organized collection of data, making it easy to store, manage, and retrieve information.

You might have just built a simple key-value store. That was great for basic data storage, but databases often offer more structure. Think of records (like rows in a spreadsheet) and fields (like columns). While full-fledged databases can manage multiple "tables" and complex relationships, our "Tiny Database" will focus on managing a single collection of structured records, stored right in a file!

Why Build a Database (even a tiny one)?

Building this project will give you a concrete understanding of:

  • CRUD Operations: The fundamental actions in data management - Create, Read, Update, and Delete.
  • Data Persistence: How programs can "remember" data between sessions by saving it to and reading from files.
  • File Formats for Data: We'll explore using JSON Lines, a simple and effective way to store structured data in text files.
  • Basic Data Modeling: Thinking about how to structure your data (our "records").
It's a fantastic way to demystify what happens inside those powerful database systems you use every day! πŸ’‘βš™οΈ

Here’s a conceptual glimpse of how you might interact with the Tiny Database we're about to build:


# Conceptual example (our actual commands will be simpler)
db = TinyDB("my_data.jsonl")

db.insert({"name": "Alice", "email": "[email protected]", "role": "Developer"})
db.insert({"name": "Bob", "email": "[email protected]", "role": "Designer"})

results = db.find_one({"name": "Alice"}) 
# Assuming find_one returns the first match or None
if results:
    print(results) 
    # Output: {"id": 1, "name": "Alice", "email": "[email protected]", "role": "Developer"} 
    # (if we implement auto-ID and find_one returns the record with its ID)

all_devs = db.find_all({"role": "Developer"})
print(f"Found {len(all_devs)} developers.")
# Output: Found 1 developers. (Or more, if others were added)
                

Ready to lay the foundation for your own data empire? πŸ“ˆ Let's get started!

Step 1: Record Design & Our Storage Blueprint πŸ“

Before we write any code to *do* things with our database, we need to decide on two fundamental aspects:

  1. How will each piece of data (a "record") be structured?
  2. How will these records be stored in a file?

Our Record Structure: Python Dictionaries πŸ“š

For simplicity and flexibility, each record in our Tiny Database will be a Python dictionary. Think of a dictionary as a collection of key-value pairs. For example, if we were storing information about books, a record might look like this:


book_record = {
    "id": 1, 
    "title": "Twenty Thousand Leagues Under the Seas", 
    "author": "Jules Verne",
    "genre": "Sci-Fi"
}
                

The keys (like "id", "title") are the names of the data fields, and the values (like `1`, "Jules Verne") are the actual pieces of information.

One crucial field is the "id". This will be a unique identifier for each record in our database. When we add new records, we'll make sure this `id` is automatically generated and always unique (e.g., by making it an auto-incrementing number). It's like the unique call number for a book in a library – essential for finding exactly what you're looking for! 🏷️

Storage Strategy: JSON Lines (JSONL) πŸ“„βž‘οΈπŸ“„

Now, how do we save these Python dictionaries to a file so the data persists? We'll use a format called JSON Lines (often with the file extension `.jsonl`).

In the JSON Lines format, each line in our file is a complete, independent JSON object (which maps perfectly to our Python dictionaries!).

Advantages of JSON Lines for our TinyDB:

  • Easy to Append: Adding a new record is as simple as adding a new line to the file. We don't need to load the entire file, parse it, add the new record, and then write everything back. This is very efficient for writes!
  • Human-Readable: You can open a `.jsonl` file in any text editor and see the data clearly.
  • Good for Streaming/Processing: It's easy to read and process one record (one line) at a time, which is memory-efficient, especially for potentially large files.

Here's how a `data.jsonl` file might look with a couple of book records:

{"id": 1, "title": "Twenty Thousand Leagues Under the Seas", "author": "Jules Verne"}
{"id": 2, "title": "Journey to the Center of the Earth", "author": "Jules Verne"}

Writing a Record with Python (JSON Lines)

To add a new record to our `data.jsonl` file, we'll open it in append mode (`'a'`). This ensures we add to the end of the file without overwriting existing data. We use `json.dumps()` to convert our Python dictionary into a JSON string.

import json

# Our new record
record = {"id": 3, "title": "Around the World in Eighty Days", "author": "Jules Verne"}

# Open the file in append mode ('a')
with open('data.jsonl', 'a') as f:
    # Convert the dictionary to a JSON string and write it, followed by a newline
    f.write(json.dumps(record) + '\n')

print("Record added to data.jsonl!") # (This is just for confirmation here)
                

Reading All Records with Python (JSON Lines)

To read all records, we open the file in read mode (`'r'`), iterate through each line, and use `json.loads()` (load string) to convert each JSON string line back into a Python dictionary.

import json

records = []
try:
    with open('data.jsonl', 'r') as f:
        for line in f:
            # .strip() removes the newline character before parsing
            if line.strip(): # Ensure line is not empty
                records.append(json.loads(line.strip())) 
except FileNotFoundError:
    print("No data file found yet. It will be created when you add a record.")

# Now 'records' is a list of dictionaries
for record_item in records: # Renamed to avoid conflict with outer 'record' variable
    print(record_item)
                

The `try-except FileNotFoundError` block is handy so our program doesn't crash if `data.jsonl` doesn't exist when we first run it. The `if line.strip():` ensures we don't try to parse empty lines, which could happen.

Test Your Knowledge! 🧠 (Section 1)

1.1: What is a key advantage of using the JSON Lines format for a simple database where new records are frequently added?

  • (A) It uses less disk space than any other format.
  • (B) It's easy to append new records without rewriting the whole file.
  • (C) It automatically creates complex relationships between records.
  • (D) It's the only format Python can read.

1.2: Which Python `json` module function is used to convert a Python dictionary into a JSON formatted string?

  • `json.load(my_dict)`
  • `json.stringify(my_dict)`
  • `json.dumps(my_dict)`
  • `json.parse(my_dict)`

Step 2: Creating Records βž•

[Content for creating records coming soon! We'll build a function to add new entries to our database file...]

Step 3: Reading Records πŸ“–

[Content for reading records coming soon! We'll explore how to fetch all records and filter by ID...]

Step 4: Updating Records πŸ”„

[Content for updating records coming soon! This will involve reading, modifying, and rewriting the file or specific lines...]

Step 5: Deleting Records πŸ—‘οΈ

[Content for deleting records coming soon! We'll look at strategies for removing data from our file-based system...]

Step 6: Simple Querying πŸ”Ž

[Content for simple querying coming soon! We'll implement ways to find records based on field values...]

πŸ§ͺ Try it Yourself Lab

Loading Pyodide... ⏳ This might take a moment.


                    

Step 2: Talking to Our Store - SET and GET Commands πŸ—£οΈ

Our dictionary `my_store` is great, but how do we actually *use* it like a real key-value store? We need a way to tell it what to do! This is where commands come in. We'll start with the two most fundamental ones:

  • SET key value: Stores a given `value` associated with a `key`.
  • GET key: Retrieves the `value` associated with a `key`.

Hearing What the User Says πŸ‘‚: Input Parsing

To make our store interactive, we need to get input from the user. Python's built-in `input()` function is perfect for this. Once we have the user's typed command (like "SET name Jules"), we need to break it apart into meaningful pieces – the command itself, the key, and potentially the value. The `split()` string method is our friend here.


# Example of getting and splitting input
user_command = input("> ")  # Prompts the user with "> "
parts = user_command.split() # Splits the string by spaces

command = parts[0] # The first part is the command
# parts[1:] will be the arguments (key, value)
print(f"Command: {command}")
print(f"Arguments: {parts[1:]}")

# Try typing: SET fruit Apple
# Output:
# Command: SET
# Arguments: ['fruit', 'Apple']
                

Remember, `split()` without arguments splits by any whitespace and handles multiple spaces gracefully. If you need to set a value that contains spaces, like "New York City", you'll need a slightly more advanced way to handle `parts`, often by joining the later parts: `value = " ".join(parts[2:])`. For now, we'll assume values don't have spaces.

Implementing SET βœ…

The SET command needs a key and a value. It stores this pair in our `my_store` dictionary. A simple way to confirm the operation is to print "OK".


my_store = {} # Our global store

def handle_set(key, value):
    my_store[key] = value
    return "OK"

# Example usage:
# result = handle_set("language", "Python")
# print(result) # Output: OK
# print(my_store) # Output: {'language': 'Python'}
                

Implementing GET πŸ”

The GET command needs a key and should return its corresponding value. What if the key isn't in our store? Real Redis returns "(nil)". We can do something similar, like returning `None` or a specific string like "(nil)" or "(key not found)". Python's dictionary `get()` method is great here, as it can return a default value if the key is missing, avoiding a `KeyError`.


# my_store should be defined as in the SET example

def handle_get(key):
    # Using dict.get(key, default_value) is safer than my_store[key]
    value = my_store.get(key) 
    if value is None:
        return "(nil)" # Or return None and let the REPL handle printing
    return f'"{value}"' # Often, string values are returned in quotes

# Example usage (assuming 'language': 'Python' is in my_store):
# print(handle_get("language")) # Output: "Python"
# print(handle_get("city"))     # Output: (nil)
                

A Simple REPL (Read-Eval-Print Loop) πŸ”„

Now let's put it all together in a loop! A REPL reads user input, evaluates it (runs our SET or GET logic), prints the result, and then loops back for more. We'll keep it simple for now.


# Make sure my_store, handle_set, and handle_get are defined as above

print("Simple Key-Value Store REPL. Type 'QUIT' to exit.")
while True:
    user_input = input("kv_store> ").strip() # Get input and remove extra whitespace
    
    if not user_input: # If user just presses Enter
        continue

    parts = user_input.split()
    command = parts[0].upper() # Convert command to uppercase for case-insensitivity

    if command == "QUIT":
        print("Exiting store. Bye! πŸ‘‹")
        break
    elif command == "SET":
        if len(parts) >= 3:
            key = parts[1]
            value = " ".join(parts[2:]) # Join all parts for the value
            print(handle_set(key, value))
        else:
            print("Error: SET command needs a key and a value. Usage: SET  ")
    elif command == "GET":
        if len(parts) >= 2:
            key = parts[1]
            print(handle_get(key))
        else:
            print("Error: GET command needs a key. Usage: GET ")
    else:
        print(f"Error: Unknown command '{command}'. Available commands: SET, GET, QUIT.")

# You can try this full REPL code in the 'Try it Yourself Lab' below!
# Example interaction:
# kv_store> SET greeting Hello World
# OK
# kv_store> GET greeting
# "Hello World"
# kv_store> GET non_existent_key
# (nil)
# kv_store> QUIT
# Exiting store. Bye! πŸ‘‹
                

This REPL is basic. It converts commands to uppercase (so "set" and "SET" both work) and handles a simple "QUIT" command. It also has very basic error checking for the number of arguments for SET and GET. We'll improve error handling in a later section!

πŸš€ You can copy the full REPL code (including the `my_store` dictionary and the `handle_set` / `handle_get` functions) into the "Try it Yourself Lab" below to run your own interactive key-value store!

Test Your Knowledge! 🧠 (Section 2)

2.1: If `user_input = "SET mykey myvalue"`, what is `user_input.split()`?

  • (A) ['SET', 'mykey', 'myvalue']
  • (B) ('SET', 'mykey', 'myvalue')
  • (C) "SET mykey myvalue" (unchanged)
  • (D) ['SET', 'mykey myvalue']

2.2: In our `handle_get(key)` function, if the key is NOT found, what is a good practice to return?

  • (A) Raise a KeyError immediately.
  • (B) Return a special value like `"(nil)"` or `None`.
  • (C) Return an empty string `""`.
  • (D) Print "Key not found" directly from the function.

Step 3: Don't Forget! - Saving Our Data to a File πŸ’Ύ

Our key-value store is getting pretty cool! But there's a big catch: all our data is stored in memory. If you close the program and run it again, poof! πŸ’¨ Everything's gone. To make our store truly useful, we need persistence – the ability to save data to a file and load it back up.

The Basics of File I/O (Input/Output) πŸ“„

Python makes working with files straightforward. The core function is `open()`, which takes a filename and a mode (like 'r' for read, 'w' for write, 'a' for append).

  • 'r': Read mode. Opens the file for reading. Error if the file doesn't exist.
  • 'w': Write mode. Opens the file for writing. Creates the file if it doesn't exist, or overwrites it if it does! Be careful!
  • 'a': Append mode. Opens for writing, but appends to the end of the file instead of overwriting.

After opening a file, you can use methods like `read()` or `write()`. Crucially, you must always `close()` the file to ensure data is properly saved and resources are freed.

A much safer way to work with files is using the with open(...) as f: statement. This automatically closes the file for you, even if errors occur.


# Writing to a file
with open("my_data.txt", "w") as f:
    f.write("Hello, file world!\n")
    f.write("This is a new line.")

# Reading from a file
with open("my_data.txt", "r") as f:
    content = f.read()
    print(content)
# Output:
# Hello, file world!
# This is a new line.
                

Turning Dictionaries into File-Friendly Data: JSON πŸ’‘

You can't just write a Python dictionary directly to a file using `f.write(my_store)`. It won't work as you expect. We need to serialize our dictionary – convert it into a string format that can be easily written to a file and then read back (deserialized) into a dictionary.

The json (JavaScript Object Notation) module is perfect for this. It's a standard text format that's human-readable and widely used.

  • json.dump(data, file_object): Writes your Python dictionary `data` to an open `file_object` in JSON format.
  • json.load(file_object): Reads JSON data from an open `file_object` and converts it back into a Python dictionary.

import json # Don't forget to import!

my_contacts = {"name": "Jules", "city": "Paris"}
filename = "contacts.json"

# Saving the dictionary to a file
with open(filename, "w") as f:
    json.dump(my_contacts, f, indent=4) # indent=4 makes it human-readable

# Loading the dictionary from the file
with open(filename, "r") as f:
    loaded_contacts = json.load(f)
    print(loaded_contacts) # Output: {'name': 'Jules', 'city': 'Paris'}
    print(loaded_contacts == my_contacts) # Output: True
                

Integrating Persistence into Our Store πŸ”„

Let's make our key-value store use a file (e.g., `my_kv_store.json`) for persistence.

  1. Define a filename: Choose a name for your data file.
  2. Load data on startup: When the store starts, try to load data from this file. If the file doesn't exist (maybe it's the first run), start with an empty dictionary.
  3. Save data on changes: After every successful `SET` (and later `DEL`) command, save the entire `my_store` dictionary to the file.

Note: Saving the entire dictionary after every write is simple but not very efficient for large stores. Real databases use more complex techniques, but this is a great starting point!


import json
import os # Needed for checking if file exists, though try-except is often better for loading

STORE_FILENAME = "my_kv_store.json"
my_store = {} # Our global store

def load_store_from_file():
    global my_store # We are modifying the global variable
    try:
        with open(STORE_FILENAME, "r") as f:
            my_store = json.load(f)
            print(f"Store loaded from {STORE_FILENAME}")
    except FileNotFoundError:
        my_store = {} # Start with an empty store if file not found
        print(f"{STORE_FILENAME} not found. Starting with an empty store.")
    except json.JSONDecodeError:
        my_store = {} # If file is corrupted, start fresh (or handle error differently)
        print(f"Error decoding JSON from {STORE_FILENAME}. Starting fresh.")


def save_store_to_file():
    with open(STORE_FILENAME, "w") as f:
        json.dump(my_store, f, indent=4)
    # print(f"Store saved to {STORE_FILENAME}") # Optional: for debugging

# --- Update handle_set ---
def handle_set(key, value):
    my_store[key] = value
    save_store_to_file() # Save after every successful set
    return "OK"

# --- Update handle_get (no change needed for saving, only reading) ---
def handle_get(key):
    value = my_store.get(key) 
    if value is None:
        return "(nil)"
    return f'"{value}"'

# --- Main REPL (Modified) ---
# load_store_from_file() # Load the store when the program starts!

# print("Simple Key-Value Store REPL (with Persistence!). Type 'QUIT' to exit.")
# while True:
#     user_input = input("kv_store_persist> ").strip()
    
#     if not user_input:
#         continue

#     parts = user_input.split()
#     command = parts[0].upper()

#     if command == "QUIT":
#         print("Exiting store. Bye! πŸ‘‹")
#         break
#     elif command == "SET":
#         if len(parts) >= 3:
#             key = parts[1]
#             # For values with spaces:
#             value = " ".join(parts[2:]) 
#             print(handle_set(key, value))
#         else:
#             print("Error: SET command needs a key and a value.")
#     elif command == "GET":
#         if len(parts) >= 2:
#             key = parts[1]
#             print(handle_get(key))
#         else:
#             print("Error: GET command needs a key.")
#     else:
#         print(f"Error: Unknown command '{command}'.")

# Try adding the load_store_from_file() call at the beginning of your REPL,
# and the save_store_to_file() call within handle_set (and later handle_del).
# You'll see your data persist across sessions!
                

Now, if you run your REPL, set some keys, quit, and run it again, your data should still be there! Magic! ✨ (Well, not magic, just file I/O and JSON).

Test Your Knowledge! 🧠 (Section 3)

3.1: What is the primary purpose of `json.dump(my_dict, f, indent=4)`?

  • (A) To load a JSON string from a file into a Python dictionary.
  • (B) To write a Python dictionary to an open file object as a JSON formatted string, with pretty-printing.
  • (C) To print the dictionary to the console with 4 spaces of indentation.
  • (D) To convert a JSON string into a Python dictionary in memory.

3.2: What is a key benefit of using `with open(filename, 'w') as f:` when writing to a file?

  • (A) It automatically creates the directory for the file if it doesn't exist.
  • (B) It encrypts the file content by default.
  • (C) It automatically closes the file when the block is exited, even if errors occur.
  • (D) It reads the entire file into memory first for faster writes.

Step 4: Cleaning Up - The DEL Command and Error Smashing πŸ’₯

Our store can remember things and even save them to a file! But what if we want to remove something? That's where the DEL key command comes in. Plus, our current REPL is a bit too trusting; if we type commands incorrectly, it might crash or do weird things. Let's add some basic error handling!

Implementing DEL πŸ—‘οΈ

The DEL command needs a key to specify which item to remove. We'll use Python's `del` keyword to remove the key-value pair from our `my_store` dictionary.

What should DEL return? Redis returns the number of keys that were removed. For a simple `DEL key` (not `DEL key1 key2 ...`), this will be 1 if the key existed and was deleted, or 0 if the key didn't exist. And yes, if persistence is active, we need to save the store after a successful deletion!


# Assume my_store, STORE_FILENAME, and save_store_to_file() are defined

def handle_del(key):
    global my_store
    if key in my_store:
        del my_store[key]
        save_store_to_file() # Save changes if persistence is on
        return "1" # Key was found and deleted
    else:
        return "0" # Key was not found

# Example usage:
# my_store = {"name": "Jules", "city": "Lyon"} # Reset for example
# save_store_to_file() # Save initial state
# print(handle_del("city"))  # Output: 1
# print(my_store)            # Output: {'name': 'Jules'}
# print(handle_del("country")) # Output: 0
                

Basic Error Smashing (Handling) πŸ”₯

Our current REPL is fragile. If you type `SET` without a key and value, or `GET` without a key, it will likely crash with an `IndexError` because it tries to access `parts[1]` or `parts[2]` that don't exist.

We can make it more robust by checking the number of `parts` (the length of the list after `split()`) before trying to access elements by their index.


# --- REPL with improved error handling (partial example) ---
# Assume my_store, handle_set, handle_get, handle_del, load_store_from_file are defined.
# load_store_from_file() # Load at start

# print("Improved Key-Value Store REPL. Type 'QUIT' to exit.")
# while True:
#     user_input = input("kv_store_robust> ").strip()
#     if not user_input:
#         continue
#     parts = user_input.split()
#     command = parts[0].upper()

#     if command == "QUIT":
#         print("Exiting. Bye! πŸ‘‹")
#         break
#     elif command == "SET":
#         if len(parts) >= 3: # Need at least 'SET', 'key', 'value'
#             key = parts[1]
#             value = " ".join(parts[2:]) # Handle multi-word values
#             print(handle_set(key, value))
#         else:
#             print("❗ Error: SET requires a key and a value. Usage: SET  ")
#     elif command == "GET":
#         if len(parts) >= 2: # Need at least 'GET', 'key'
#             key = parts[1]
#             print(handle_get(key))
#         else:
#             print("❗ Error: GET requires a key. Usage: GET ")
#     elif command == "DEL":
#         if len(parts) >= 2: # Need at least 'DEL', 'key'
#             key = parts[1]
#             print(handle_del(key))
#         else:
#             print("❗ Error: DEL requires a key. Usage: DEL ")
#     else:
#         print(f"❗ Error: Unknown command '{command}'. Available: SET, GET, DEL, QUIT.")

# Example Interactions:
# kv_store_robust> SET name Pixel
# OK
# kv_store_robust> GET name
# "Pixel"
# kv_store_robust> DEL name
# 1
# kv_store_robust> GET name
# (nil)
# kv_store_robust> SET
# ❗ Error: SET requires a key and a value. Usage: SET  
# kv_store_robust> GET
# ❗ Error: GET requires a key. Usage: GET 
# kv_store_robust> DEL
# ❗ Error: DEL requires a key. Usage: DEL 
                

This is just the tip of the iceberg for error handling! Proper error handling can involve more specific checks (e.g., are keys/values valid?), custom error types, and more user-friendly messages. But even these simple `len(parts)` checks make our store much more usable.

πŸš€ Feel free to integrate `handle_del` and the improved error checks into the full REPL code in the "Try it Yourself Lab" below!

Test Your Knowledge! 🧠 (Section 4)

4.1: What does `del my_store[key]` do if `key` exists in `my_store`?

  • (A) Removes the key-value pair associated with `key` from `my_store`.
  • (B) Sets the value of `key` in `my_store` to `None`.
  • (C) Empties the entire `my_store` dictionary.
  • (D) Returns the value of `my_store[key]` before deleting it.

4.2: Why is checking `len(parts)` important before accessing `parts[1]` or `parts[2]` in our REPL's command handling?

  • (A) To ensure the command is in uppercase.
  • (B) To prevent an `IndexError` if the user provides too few arguments for the command.
  • (C) To check if the key already exists in the store.
  • (D) To convert arguments to the correct data type.

Conclusion: You Built a KV Store! What's Next? πŸ†πŸŒŸ

Congratulations! You've successfully built your very own simple key-value store from scratch using Python! That's a fantastic achievement. πŸŽ‰

Let's quickly recap what you've accomplished:

  • Started with a basic in-memory store using a Python dictionary.
  • Implemented core commands: SET, GET, and DEL.
  • Built a REPL (Read-Eval-Print Loop) to interact with your store.
  • Added persistence by saving and loading data to/from a JSON file. πŸ’Ύ
  • Implemented basic error handling for command parsing.
You've touched upon some fundamental concepts in data storage and program design. Give yourself a pat on the back!

Reality Check: Limitations of Our Mini-Store πŸ€”

While our store is awesome for learning, it's important to understand its limitations compared to production-grade systems like Redis:

  • Efficiency: Saving the entire store to a file after every write (`SET` or `DEL`) is very inefficient for large amounts of data. Real databases use more sophisticated techniques like append-only files, write-ahead logging, or specific data structures.
  • Concurrency: Our store is not thread-safe. If multiple users or processes tried to access it at the same time, data corruption could occur.
  • Data Types: We only store strings (or data that can be easily converted to/from strings via JSON). Redis supports various data structures like lists, sets, hashes, etc.
  • Advanced Features: Production systems offer many more features: transactions, expiry of keys, replication, clustering, advanced querying, etc.
  • Robustness: Our error handling is basic. Real systems have extensive error checking and recovery mechanisms.

Ready for More Adventures? Further Exploration! πŸš€

This project is a great stepping stone. Here are some ideas if you're itching to explore further and build upon what you've learned:

  • More Commands:
    • EXISTS key: Check if a key exists.
    • KEYS *: List all keys (be careful with large stores!).
    • INCR key / DECR key: Atomic increment/decrement for numeric values.
  • Improved Data Validation: Add checks for valid key names or value types.
  • Different Serialization: Explore Python's `pickle` module (for Python-specific objects, but be aware of security implications) or a custom text-based format.
  • More Efficient Saving: Think about how you could avoid rewriting the whole file every time. Maybe an append-only log for changes?
  • Networking: Try to make your store accessible over a network using Python's `socket` module or a web framework like Flask/FastAPI. (This is a big step!)
  • Type System for Values: Allow storing numbers or booleans and have `GET` return them in their correct Python type.

The world of software development is all about continuous learning and building. You've taken a significant step by completing this project. Keep that curiosity alive, tackle new challenges, and don't be afraid to break things and learn from them.

Happy Coding! ✨ Keep Building! πŸš€

© 2023 AllDayCode. All rights reserved. Built with Tailwind CSS & Pyodide.