1
0
mirror of https://gitlab.com/2-chainz/2chainz.git synced 2025-06-16 01:46:39 +00:00

Compare commits

...

18 Commits

Author SHA1 Message Date
08c4b96918 Remove bad CLI arg 2025-05-23 15:51:39 -06:00
6bb53f6f7d try this 2025-05-23 15:48:30 -06:00
69238678b8 sort imports 2025-05-23 15:38:40 -06:00
964ba43a2b format 2025-05-23 15:36:37 -06:00
455fd28bc2 setup python formatting 2025-05-23 15:35:54 -06:00
db169159e6 Add Proxy Headers 2025-05-23 15:31:59 -06:00
3a1c7a904c Make API work with cloudflare 2025-05-23 15:23:45 -06:00
b5f8dae3b9 fix port 2025-05-23 14:58:40 -06:00
8263480b72 change mount types for kaniko 2025-05-23 14:51:58 -06:00
05f2bba568 Fix staging 2025-05-23 14:49:06 -06:00
e152f9e4ad lets build a container? 2025-05-23 14:45:44 -06:00
972ce7c8cb do ruff stuff in CI 2025-05-23 14:35:11 -06:00
fdcf5be4c9 Run Pytest in the pipeline 2025-05-23 14:32:34 -06:00
99a07a7493 clean up html 2025-05-23 14:30:30 -06:00
d05cc46660 Add tests 2025-05-23 14:28:17 -06:00
a683fceb10 get everything working together kinda 2025-05-23 13:53:40 -06:00
5687871b86 clean up test 2025-05-23 12:04:51 -06:00
e488f979c1 Accidentally didn't push this stuff and deleted it last night lol 2025-05-23 12:02:31 -06:00
9 changed files with 294 additions and 78 deletions

View File

@ -1,22 +1,27 @@
// For format details, see https://aka.ms/devcontainer.json. For config options, see the // For format details, see https://aka.ms/devcontainer.json. For config options, see the
// README at: https://github.com/devcontainers/templates/tree/main/src/debian // README at: https://github.com/devcontainers/templates/tree/main/src/debian
{ {
"name": "Debian", "name": "Debian",
// Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
"image": "mcr.microsoft.com/devcontainers/base:bullseye", "image": "mcr.microsoft.com/devcontainers/base:bullseye",
"features": { "features": {
"ghcr.io/va-h/devcontainers-features/uv:1": {} "ghcr.io/va-h/devcontainers-features/uv:1": {}
} },
"customizations": {
"vscode": {
"extensions": ["tamasfe.even-better-toml"]
}
}
// Features to add to the dev container. More info: https://containers.dev/features. // Features to add to the dev container. More info: https://containers.dev/features.
// "features": {}, // "features": {},
// Use 'forwardPorts' to make a list of ports inside the container available locally. // Use 'forwardPorts' to make a list of ports inside the container available locally.
// "forwardPorts": [], // "forwardPorts": [],
// Configure tool-specific properties. // Configure tool-specific properties.
// "customizations": {}, // "customizations": {},
// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
// "remoteUser": "root" // "remoteUser": "root"
} }

View File

@ -1,10 +1,51 @@
pages: variables:
stage: deploy UV_VERSION: 0.5
PYTHON_VERSION: 3.12
BASE_LAYER: bookworm-slim
# GitLab CI creates a separate mountpoint for the build directory,
# so we need to copy instead of using hard links.
UV_LINK_MODE: copy
.base_ruff:
stage: build
interruptible: true
image:
name: ghcr.io/astral-sh/ruff:0.11.10-alpine
before_script:
- cd $CI_PROJECT_DIR
- ruff --version
Ruff Check:
extends: .base_ruff
script: script:
- mkdir public - ruff check --output-format=gitlab > code-quality-report.json
- cp -r website/* public/
artifacts: artifacts:
paths: reports:
- public codequality: $CI_PROJECT_DIR/code-quality-report.json
only:
- main Ruff Format:
extends: .base_ruff
script:
- ruff format --diff
pytest:
image: ghcr.io/astral-sh/uv:$UV_VERSION-python$PYTHON_VERSION-$BASE_LAYER
stage: test
script:
- uv run pytest --junitxml=report.xml
artifacts:
when: always
reports:
junit: report.xml
create_container:
stage: deploy
image:
name: gcr.io/kaniko-project/executor:v1.23.2-debug
entrypoint: [""]
script:
- /kaniko/executor
--context "${CI_PROJECT_DIR}"
--dockerfile "${CI_PROJECT_DIR}/Dockerfile"
--destination "${CI_REGISTRY_IMAGE}:${CI_COMMIT_BRANCH}"
--cleanup

6
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,6 @@
{
"editor.formatOnSave": true,
"[python]": {
"editor.defaultFormatter": "charliermarsh.ruff"
}
}

26
Dockerfile Normal file
View File

@ -0,0 +1,26 @@
# Use a Python image with uv pre-installed
FROM ghcr.io/astral-sh/uv:python3.12-bookworm-slim
# Install the project into `/app`
WORKDIR /app
# Enable bytecode compilation
ENV UV_COMPILE_BYTECODE=1
# Copy from the cache instead of linking since it's a mounted volume
ENV UV_LINK_MODE=copy
# Copy dependency files
COPY uv.lock pyproject.toml ./
# Install the project's dependencies using the lockfile and settings
RUN --mount=type=cache,target=/root/.cache/uv \
uv sync --locked --no-install-project --no-dev
# Then, add the rest of the project source code and install it
COPY . /app
RUN --mount=type=cache,target=/root/.cache/uv \
uv sync --locked --no-dev
CMD ["uv", "run", "fastapi", "run", "src/two_chainz", "--port", "80", "--proxy-headers"]

View File

@ -1,50 +1,50 @@
quotes = [ quotes = [
"My side chick got pregnant by her main dude and i'm offended.", "My side chick got pregnant by her main dude and i'm offended",
"I kiss your lady, eat her pussy, then kiss the baby.", "I kiss your lady, eat her pussy, then kiss the baby",
"Left hand on that steering wheel, right hand on that pussy!", "Left hand on that steering wheel, right hand on that pussy!",
"For my birthday I threw me a surprise party!", "For my birthday I threw me a surprise party!",
"She got a big booty so I call her big booty", "She got a big booty so I call her big booty",
"My girl got a big purse with a purse in it, and her pussy so clean, I can go to church in it!", "My girl got a big purse with a purse in it, and her pussy so clean, I can go to church in it!",
"Beat the pussy up, I need riot gear.", "Beat the pussy up, I need riot gear",
"Gas in a Ziplock, now thats loud and clear.", "Gas in a Ziplock, now thats loud and clear",
"My wrist deserve a shout out, 'I'm like what up wrist?'\nMy stove deserve a shout out, 'I'm like what up stove?'", "My wrist deserve a shout out, 'I'm like what up wrist?'\nMy stove deserve a shout out, 'I'm like what up stove?'",
"I'm in the kitchen. Yams errrrrwhere.", "I'm in the kitchen. Yams errrrrwhere",
"I encourage everyone to pay attention to the issues that matter to you, from jobs and the economy, to education and our schools, to criminal justice reform. Whatever it is that you care about, make sure you use your voice.", "I encourage everyone to pay attention to the issues that matter to you, from jobs and the economy, to education and our schools, to criminal justice reform. Whatever it is that you care about, make sure you use your voice",
"If you a chicken head, go somewhere and lay some eggs", "If you a chicken head, go somewhere and lay some eggs",
"Chain hang to my ding-a-ling, chain hang, chain hang to my ding-a-ling", "Chain hang to my ding-a-ling, chain hang, chain hang to my ding-a-ling",
"I tried to get a tan but I'm black already.", "I tried to get a tan but I'm black already",
"Then I put a fat rabbit on a Craftmatic!", "Then I put a fat rabbit on a Craftmatic!",
"Yeah, I love them strippers", "Yeah, I love them strippers",
"If I wasn't rapping I'd be trapping.", "If I wasn't rapping I'd be trapping",
"Started from the trap, now I rap", "Started from the trap, now I rap",
"I'm so high I can sing to a chandelier\nMy flow a glass of Ace of Spade and yours a can of beer.", "I'm so high I can sing to a chandelier\nMy flow a glass of Ace of Spade and yours a can of beer",
"I look you right in your face, sing to your bitch like I'm Drake!", "I look you right in your face, sing to your bitch like I'm Drake!",
"Ass so big, I told her to look back at it!", "Ass so big, I told her to look back at it!",
"Drunk and high at the same time.\nDrinking champagne on an airplane!", "Drunk and high at the same time.\nDrinking champagne on an airplane!",
"Wood grain, chestnut\nTitty fuck, chest nut", "Wood grain, chestnut\nTitty fuck, chest nut",
"Horsepower, horsepower, all this Polo on, I got horsepower", "Horsepower, horsepower, all this Polo on, I got horsepower",
"If I die, bury me inside the Louis store", "If I die, bury me inside the Louis store",
"GI-VEN-CHY, nigga God Bless you.", "GI-VEN-CHY, nigga God Bless you",
"My favorite dish is turkey lasagna\nEven my pajamas designer.", "My favorite dish is turkey lasagna\nEven my pajamas designer",
"Walked in, ill nigga alert! ill nigga alert!", "Walked in, ill nigga alert! ill nigga alert!",
"Like fuck your baby daddy, his daddy should've worn a condom", "Like fuck your baby daddy, his daddy should've worn a condom",
"I'm the type of nigga thats built to last.\nYou fuck with me, Ill put my foot in your ass.", "I'm the type of nigga thats built to last.\nYou fuck with me, Ill put my foot in your ass",
"I wish a nigga would like a kitchen cabinet.", "I wish a nigga would like a kitchen cabinet",
"Louie V is my kryptonite.", "Louie V is my kryptonite",
"If you woke up this morning, Nigga you winnin!", "If you woke up this morning, Nigga you winnin!",
"I use good pussy like its lotion.", "I use good pussy like its lotion",
"Big shit like a dinosaur did it.", "Big shit like a dinosaur did it",
"#making bands, yes I am.", "#making bands, yes I am",
"I wear versace like its nike, you don't like it do you?", "I wear versace like its nike, you don't like it do you?",
"I bet you feel this bank roll if I bump into you.", "I bet you feel this bank roll if I bump into you",
"Like fuck your baby daddy, his daddy should've wore a condom.", "Like fuck your baby daddy, his daddy should've wore a condom",
"Sprinter van on me, I got them xans on me,\nDriveway so damn long by the time I leave I'm damn asleep", "Sprinter van on me, I got them xans on me,\nDriveway so damn long by the time I leave I'm damn asleep",
"Bitches round my pool, I made them hoes look like my landscape.", "Bitches round my pool, I made them hoes look like my landscape",
"Everything Proper, no propaganda.", "Everything Proper, no propaganda",
"Big sack, a lotta hoes like Santa", "Big sack, a lotta hoes like Santa",
"Attitude on some 'Fuck you too!'\nBankroll on 'What it do, boo?'", "Attitude on some 'Fuck you too!'\nBankroll on 'What it do, boo?'",
"I got a pocket full of money, it got me walking all slew-foot", "I got a pocket full of money, it got me walking all slew-foot",
"I'm on my wave like a cruise ship.", "I'm on my wave like a cruise ship",
"In that hoe mouth like a toothpick", "In that hoe mouth like a toothpick",
"My new bitch gon' pull me a new bitch,\nThen pull me a new bitch\nSee that is a snowball effect", "My new bitch gon' pull me a new bitch,\nThen pull me a new bitch\nSee that is a snowball effect",
"I got a mansion full of marble floors,\nIt look like I could go bowl in this bitch", "I got a mansion full of marble floors,\nIt look like I could go bowl in this bitch",

View File

@ -3,30 +3,19 @@ name = "2chainz"
version = "0.1.0" version = "0.1.0"
description = "Add your description here" description = "Add your description here"
readme = "README.md" readme = "README.md"
authors = [ authors = [{ name = "Anson", email = "anson@ansonbiggs.com" }]
{ name = "Anson", email = "anson@ansonbiggs.com" }
]
requires-python = ">=3.13" requires-python = ">=3.13"
dependencies = [ dependencies = ["fastapi[standard]>=0.115.12"]
"fastapi[standard]>=0.115.12",
]
[project.scripts] [project.scripts]
two_chainz = "two_chainz:main" two_chainz = "two_chainz:main"
[build-system] [build-system]
requires = ["hatchling"] requires = ["hatchling"]
build-backend = "hatchling.build" build-backend = "hatchling.build"
[tool.hatch.build.targets.wheel] [tool.hatch.build.targets.wheel]
packages = ["src/two_chainz"] packages = ["src/two_chainz"]
[dependency-groups] [dependency-groups]
dev = [ dev = ["httpx>=0.28.1", "pytest>=8.3.5", "ruff>=0.11.11"]
"httpx>=0.28.1",
"pytest>=8.3.5",
"ruff>=0.11.11",
]

View File

@ -0,0 +1,88 @@
from datetime import datetime
from unittest.mock import patch
import pytest
from fastapi.testclient import TestClient
import two_chainz
from two_chainz import app
client = TestClient(app, base_url="http://chainz.ansonbiggs.com")
class TestApi:
def test_api_endpoint(self):
response = client.get("/api/")
assert response.status_code == 200
data = response.json()
assert "status" in data
assert data["status"] == "ok"
assert "timestamp" in data
assert "uptime_seconds" in data
# Validate timestamp format (ISO format)
assert datetime.fromisoformat(data["timestamp"])
@pytest.mark.parametrize(
"mocked_time,start_time,expected",
[
(100, 50, 50), # 100 - 50 = 50 seconds uptime
(200, 100, 100), # 200 - 100 = 100 seconds uptime
],
)
def test_api_uptime_calculation(self, mocked_time, start_time, expected):
with patch("time.time", return_value=mocked_time):
with patch("two_chainz.start_time", start_time):
response = client.get("/api/")
assert response.json()["uptime_seconds"] == expected
@pytest.mark.parametrize("endpoint", ["/api/quote", "/api/alias"])
class TestDataEndpoints:
def test_endpoint_nonempty(self, endpoint):
for _ in range(1000): # Data is random so run the test a ton
# Given we have an endpoint
# When we do a get request
response = client.get(endpoint)
# Then we should get a 200 status code
assert response.status_code == 200
data = response.json()
# Then there should be a single entry in the dict
assert len(data) == 1
# Then the values should not be empty
key, value = next(iter(data.items()))
assert key
assert value
class TestData:
def test_data_exists(self):
assert two_chainz.data
assert two_chainz.data["quotes"]
assert two_chainz.data["aliases"]
@pytest.mark.parametrize("quote", two_chainz.data["quotes"])
class TestQuotes:
def test_no_empty(self, quote):
assert quote
def test_no_ending_period(self, quote):
assert quote[-1] != "."
def test_no_ending_newline(self, quote):
assert quote[-1] != "\n"
@pytest.mark.parametrize("alias", two_chainz.data["quotes"])
class TestAlias:
def test_no_empty(self, alias):
assert alias
def test_no_ending_newline(self, alias):
assert alias[-1] != "\n"

View File

@ -0,0 +1,70 @@
import random
import time
import tomllib
from datetime import datetime
from pathlib import Path
import logging
from fastapi import FastAPI, Request
from fastapi.middleware.trustedhost import TrustedHostMiddleware
from fastapi.staticfiles import StaticFiles
app = FastAPI()
app.add_middleware(
TrustedHostMiddleware,
allowed_hosts=["*"],
)
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
start_time = time.time()
def read_data() -> dict[str, str]:
raw_data = tomllib.loads(Path("data.toml").read_text())
raw_data["aliases"] = [
alias["name"] for alias in raw_data["aliases"] for _ in range(alias["weight"])
]
return raw_data
data = read_data()
# Log all requests to debug issues
@app.middleware("http")
async def log_requests(request: Request, call_next):
# Log what's coming in
logger.info(f"Request: {request.method} {request.url.path}")
logger.info(f"Host header: {request.headers.get('host')}")
logger.info(f"CF-Ray: {request.headers.get('cf-ray', 'Not from Cloudflare')}")
try:
response = await call_next(request)
return response
except Exception as e:
logger.error(f"Request processing failed: {e}")
raise
@app.get("/api/")
async def ping():
return {
"status": "ok",
"timestamp": datetime.now().isoformat(),
"uptime_seconds": round(time.time() - start_time, 2),
}
@app.get("/api/quote")
async def quote():
return {"quote": random.choice(data["quotes"])}
@app.get("/api/alias")
async def alias():
return {"alias": random.choice(data["aliases"])}
# Mount static files
app.mount("/", StaticFiles(directory="website", html=True), name="static")

View File

@ -1,11 +1,13 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8" />
<link rel="icon" type="image/png" href="favicon-32x32.png" sizes="32x32" /> <link rel="icon" type="image/png" href="favicon-32x32.png" sizes="32x32" />
<link rel="icon" type="image/png" href="favicon-16x16.png" sizes="16x16" /> <link rel="icon" type="image/png" href="favicon-16x16.png" sizes="16x16" />
<link rel="stylesheet" href="css/mvp.css" /> <link rel="stylesheet" href="css/mvp.css" />
<meta charset="utf-8" /> <title>2chainz.ansonbiggs.com</title>
<meta name="description" content="A REST API for 2 Chainz Quotes." /> <meta name="description" content="A REST API for 2 Chainz Quotes." />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta property="og:title" content="2 Chainz Rest API" /> <meta property="og:title" content="2 Chainz Rest API" />
@ -14,16 +16,6 @@
property="og:description" property="og:description"
content="A free REST API for 2 Chainz quotes" content="A free REST API for 2 Chainz quotes"
/> />
<meta property="og:type" content="website" />
<meta name="twitter:card" content="summary" />
<meta name="twitter:creator" content="@Anson_3D" />
<meta name="twitter:url" content="https://2chainz.ansonbiggs.com" />
<meta name="twitter:title" content="2chainz.ansonbiggs.com" />
<meta
name="twitter:description"
content="A free REST API for 2 Chainz quotes"
/>
<style> <style>
* { * {
@ -56,7 +48,6 @@
} }
} }
</style> </style>
<title>2chainz.ansonbiggs.com</title>
</head> </head>
<body> <body>
@ -109,8 +100,8 @@
<p> <p>
send a <code>get</code> request to send a <code>get</code> request to
<code <code
><a href="https://chainz-rest.azurewebsites.net/quote" ><a href="https://chainz.ansonbiggs.com/api/quote"
>https://chainz-rest.azurewebsites.net/quote</a >https://chainz.ansonbiggs.com/api/quote</a
></code ></code
> >
</p> </p>
@ -132,8 +123,8 @@
> >
and is subject to change. An example return from and is subject to change. An example return from
<code> <code>
<a href="https://chainz-rest.azurewebsites.net/quote?batch=2" <a href="https://chainz.ansonbiggs.com/api/quote?batch=2"
>https://chainz-rest.azurewebsites.net/quote?batch=2</a >https://chainz.ansonbiggs.com/api/quote?batch=2</a
></code ></code
> >
</p> </p>
@ -160,8 +151,8 @@
<p> <p>
send a <code>get</code> request to send a <code>get</code> request to
<code <code
><a href="https://chainz-rest.azurewebsites.net/alias" ><a href="https://chainz.ansonbiggs.com/api/alias"
>https://chainz-rest.azurewebsites.net/alias</a >https://chainz.ansonbiggs.com/api/alias</a
></code ></code
> >
</p> </p>
@ -275,7 +266,7 @@
getQuote(); getQuote();
function getQuote() { function getQuote() {
fetch("https://chainz-rest.azurewebsites.net/quote", { method: "GET" }) fetch("/api/quote", { method: "GET" })
.then((resp) => resp.json()) .then((resp) => resp.json())
.then(function (data) { .then(function (data) {
document.getElementById("quote").innerHTML = data.quote; document.getElementById("quote").innerHTML = data.quote;
@ -288,7 +279,7 @@
).href = `https://twitter.com/intent/tweet?text=${tweet}`; ).href = `https://twitter.com/intent/tweet?text=${tweet}`;
}); });
fetch("https://chainz-rest.azurewebsites.net/alias", { method: "GET" }) fetch("/api/alias", { method: "GET" })
.then((resp) => resp.json()) .then((resp) => resp.json())
.then(function (data) { .then(function (data) {
document.getElementById("alias").innerHTML = "- " + data.alias; document.getElementById("alias").innerHTML = "- " + data.alias;