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
29 changes: 29 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
name: CI

on:
pull_request:
push:
branches:
- main

jobs:
test:
runs-on: ubuntu-latest

steps:
- name: Check out repository
uses: actions/checkout@v4

- name: Install uv
uses: astral-sh/setup-uv@v6

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"

- name: Install dependencies
run: uv sync --extra dev

- name: Run tests
run: uv run python -m pytest
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ wheels/
.pytest_cache/
.coverage
htmlcov/
matching_results.txt

# IDE
.idea/
Expand All @@ -35,6 +36,7 @@ htmlcov/

# Environment
.env
.venv/
venv/
env/
ENV/
Expand Down
11 changes: 11 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
- id: check-added-large-files
- id: check-toml
- id: check-yaml
- id: end-of-file-fixer
exclude: ^(frontend/|graph_viz\.ipynb|match_graph\.html)
- id: trailing-whitespace
exclude: ^(frontend/|graph_viz\.ipynb|match_graph\.html)
61 changes: 61 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1 +1,62 @@
# Furever Match

Find the right home for every dog, and the right dog for every home.

Furever Match is a bilingual adoption matching app built for rescue teams and adopters who want a warmer, smarter way to make adoption decisions. It turns adopter needs, home context, lifestyle details, and dog profiles into practical match recommendations that are easy to understand and act on.

The app combines a Hebrew-first onboarding flow, a Flask API, Supabase-backed dog and adoption-request data, and a rules-based matching engine with optional LLM-assisted personality reasoning.

## What It Feels Like

An adopter moves through a friendly quiz about their home, schedule, family, other pets, experience, and preferences. Behind the scenes, Furever Match filters out unsafe or unsuitable matches, scores the remaining dogs, and presents compatible candidates with clear explanations instead of a flat catalogue.

For rescue teams, the goal is simple: fewer generic applications, better conversations, and matches that respect both the adopter's reality and each dog's needs.

## Why It Matters

Good adoption matching is not just about breed, size, or age. It depends on daily rhythm, energy, training expectations, children, other pets, living space, and personality. Furever Match keeps those signals visible, structured, and explainable.

## Highlights

- Hebrew-first adopter experience with English-normalized backend data
- Lifestyle quiz for home, care routine, activity level, and preferences
- Hard filters for clear incompatibilities like young children or existing pets
- Weighted soft scoring for better-fit recommendations
- Optional LLM reasoning for personality and character alignment
- Supabase integration for dogs, images, and adoption requests
- Lightweight Flask API that also serves the frontend prototype

## Tech Snapshot

- Python, Flask, and Flask-CORS
- Supabase for persistence
- PyYAML-driven matching rules
- Optional Ollama or Gemini-backed LLM analysis
- Static HTML/CSS/JavaScript frontend
- `uv` for repeatable local and CI setup

## Run It Locally

```bash
uv sync --extra dev
uv run python run.py
```

Open `http://localhost:8000`.

Database-backed screens and API routes require:

```bash
SUPABASE_URL=...
SUPABASE_KEY=...
```

To run the test suite:

```bash
uv run python -m pytest
```

## Project Status

This is an early-stage DataHackIL prototype focused on making dog adoption matching more thoughtful, transparent, and inviting. The product direction is intentionally practical: help shelters and adopters reach better first conversations, then better forever matches.
4 changes: 2 additions & 2 deletions frontend_stitch.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
None selected
None selected


Skip to content
Expand Down Expand Up @@ -1906,4 +1906,4 @@ hadas14@gmail.com. Press tab to insert.
</nav>
</body></html>
stitch_all.txt
Displaying stitch_all.txt.
Displaying stitch_all.txt.
28 changes: 22 additions & 6 deletions furever_match/db_ingestion.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,28 @@

load_dotenv()

supabase = create_client(
os.environ["SUPABASE_URL"],
os.environ["SUPABASE_KEY"]
)
class SupabaseProxy:
"""Create the Supabase client only when a database operation is requested."""

def __init__(self):
self._client = None

def _get_client(self):
if self._client is None:
url = os.getenv("SUPABASE_URL")
key = os.getenv("SUPABASE_KEY")
if not url or not key:
raise RuntimeError(
"SUPABASE_URL and SUPABASE_KEY must be set for database operations."
)
self._client = create_client(url, key)
return self._client

def __getattr__(self, name):
return getattr(self._get_client(), name)


supabase = SupabaseProxy()

# -----------------------------
# Helpers (CLEANING LOGIC)
Expand Down Expand Up @@ -250,5 +268,3 @@ def ingest_adoption_request(raw_request):
request_id = response.data[0]["id"]
print(f"Inserted adoption request {request_id}")
return request_id


2 changes: 1 addition & 1 deletion furever_match/matching_rules.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -286,4 +286,4 @@ score_thresholds:
very_good: 75 # 75-89 → "Recommended"
good: 60 # 60-74 → "Consider"
moderate: 40 # 40-59 → "Requires discussion"
poor: 0 # 0-39 → "Not recommended"
poor: 0 # 0-39 → "Not recommended"
6 changes: 3 additions & 3 deletions furever_match/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,14 @@ class DogProfile(BaseModel):
age_group: str = Field(description="Age category, e.g., 'puppy', 'adult', 'senior'")
is_neutralized: bool = Field(description="True if the dog is spayed/neutered")
cat_friendly: bool = Field(description="True if the dog is friendly or compatible with cats")

# Using a 1-5 scale for scoring simplicity
energy_level: int = Field(ge=1, le=5, description="Energy level of the dog on a scale from 1 (lowest) to 5 (highest)")
size: int = Field(ge=1, le=5, description="Size of the dog on a scale from 1 (smallest) to 5 (largest)")

# Generic compatibility dictionary, e.g., {"kids": 4, "apartment": 3, "other_dogs": 5}
compatibility_score: Dict[str, int] = Field(
default_factory=dict,
default_factory=dict,
description="Dictionary mapping compatibility categories (like 'kids', 'apartment') to a score (e.g., 1-5)"
)

Expand Down
2 changes: 1 addition & 1 deletion migrate_dog_llm_features.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,4 +76,4 @@ def main():


if __name__ == "__main__":
main()
main()
20 changes: 15 additions & 5 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,27 @@ build-backend = "setuptools.build_meta"
[project]
name = "furever-match"
version = "0.1.0"
description = "An app to match forever homes for pets"
requires-python = ">=3.8"
description = "A bilingual adoption matching app that helps dogs and people find better-fit forever homes."
requires-python = ">=3.10"
authors = [
{name = "Your Name", email = "your.email@example.com"}
{name = "DataHackIL"}
]
dependencies = [
# Add your dependencies here
"beautifulsoup4>=4.12.0",
"flask>=3.0.3",
"flask-cors>=5.0.0",
"httpx>=0.27.0",
"pydantic>=2.0.0",
"python-dotenv>=1.0.0",
"pyyaml>=6.0",
"supabase>=2.0.0",
"typing-extensions>=4.0.0",
]

[project.optional-dependencies]
dev = [
"pytest>=8.0.0",
]

[project.urls]
Homepage = "https://github.com/yourusername/furever_match"
Homepage = "https://github.com/DataHackIL/furever_match"
2 changes: 1 addition & 1 deletion pytest.ini
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
[tool:pytest]
[pytest]
testpaths = tests
python_files = test_*.py
python_classes = Test*
Expand Down
23 changes: 15 additions & 8 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,23 @@
setup(
name="furever-match",
version="0.1.0",
description="An app to match forever homes for pets",
author="Your Name",
author_email="your.email@example.com",
url="https://github.com/yourusername/furever_match",
description="A bilingual adoption matching app that helps dogs and people find better-fit forever homes.",
author="DataHackIL",
url="https://github.com/DataHackIL/furever_match",
packages=find_packages(),
python_requires=">=3.8",
python_requires=">=3.10",
install_requires=[
# Add your dependencies here
"beautifulsoup4>=4.12.0",
"flask>=3.0.3",
"flask-cors>=5.0.0",
"httpx>=0.27.0",
"pydantic>=2.0.0",
"python-dotenv>=1.0.0",
"pyyaml>=6.0",
"supabase>=2.0.0",
"typing-extensions>=4.0.0",
],
extras_require={"dev": ["pytest>=8.0.0"]},
entry_points={
"console_scripts": [
"furever-match=furever_match.main:main",
Expand All @@ -21,9 +29,8 @@
"Development Status :: 3 - Alpha",
"Intended Audience :: Developers",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
],
)
18 changes: 9 additions & 9 deletions tests/test_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,19 @@
Tests for the main module
"""

import pytest
from furever_match.main import App
from furever_match.main import app


def test_app_initialization():
"""Test App initialization"""
app = App()
assert app is not None
assert isinstance(app.config, dict)


def test_app_run():
"""Test App run method"""
app = App()
# This should not raise an exception
app.run()
def test_health_check():
response = app.test_client().get("/api/health")

assert response.status_code == 200
assert response.get_json() == {
"status": "ok",
"message": "FureverMatch API is running",
}
Loading
Loading