Skip to content

Commit

Permalink
Add function to bootstrap the first generation. (#111)
Browse files Browse the repository at this point in the history
* Add function to bootstrap the first generation.

* Remove pipenv from docker-compose commands.
  • Loading branch information
anybodys authored Sep 23, 2024
1 parent 2a2bbc7 commit 961d15d
Show file tree
Hide file tree
Showing 34 changed files with 566 additions and 396 deletions.
Original file line number Diff line number Diff line change
@@ -1,23 +1,20 @@
name: CI Painter API
name: CI API

on:
push:
paths:
- painterapi/**
- .github/workflows/ci.painterapi.yaml
- api/**
- .github/workflows/ci.api.yaml

defaults:
run:
working-directory: painterapi
working-directory: api

jobs:
build:

runs-on: ubuntu-latest

env:
DISPLAY: :99.0

steps:
- uses: actions/checkout@v2

Expand All @@ -27,10 +24,6 @@ jobs:
with:
python-version: "3.10"


- run: sudo apt-get install xvfb
- run: Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 &

- name: Install pipenv
run: python -m pip install --upgrade pipenv

Expand Down
3 changes: 3 additions & 0 deletions api/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ POSTGRES_DB=artist
POSTGRES_USER=local-user
POSTGRES_PASSWORD=local-pass

# No display for graphics rendering.
DISPLAY=":99.0"

# Google
GOOGLE_APPLICATION_CREDENTIALS="/path/to/your/iam/serice/account.json"
GOOGLE_CLOUD_PROJECT="artist-2d"
Expand Down
6 changes: 5 additions & 1 deletion api/.env.test
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,16 @@
ENV="test"
SECRET_KEY="django-insecure-test-key"
PORT=8080
POSTGRES_HOST=db
# This matches te docker-compose service.
POSTGRES_HOST=db-test
POSTGRES_PORT=5432
POSTGRES_DB=artist
POSTGRES_USER=postgres
POSTGRES_PASSWORD=test-pass

# No display for graphics rendering.
DISPLAY=":99.0"

# Google
GOOGLE_APPLICATION_CREDENTIALS="/path/to/your/iam/serice/account.json"
GOOGLE_CLOUD_PROJECT="artist-2d"
Expand Down
5 changes: 5 additions & 0 deletions api/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ RUN PIPENV_VENV_IN_PROJECT=1 pipenv install --deploy

FROM base AS runtime

RUN apt-get update && apt-get install -y --no-install-recommends \
python3-tk \
ghostscript \
&& rm -rf /var/lib/apt/lists/*

# Copy virtual env from python-deps stage
COPY --from=python-deps /.venv /.venv
ENV PATH="/.venv/bin:$PATH"
Expand Down
21 changes: 12 additions & 9 deletions api/Makefile
Original file line number Diff line number Diff line change
@@ -1,25 +1,28 @@
.PHONY: setup run run-api test testcase deploy clean
.PHONY: setup setup-virtual-display run run-api db-migrate db-makemigrations db-shell test clean

setup:
pip install --upgrade --user pipenv
pipenv install --dev

setup-virtual-display:
Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 &

run: run-api

run-api:
pipenv run docker-compose up -d --build
run-api: setup-virtual-display
docker compose up -d --build

db-migrate:
pipenv run docker-compose exec api python manage.py migrate --noinput
docker compose exec api python manage.py migrate --noinput

db-makemigrations:
pipenv run docker-compose exec api python manage.py makemigrations --noinput
docker compose exec api python manage.py makemigrations --noinput

db-shell:
pipenv run docker-compose exec db psql artist local-user
db-shell: run
docker compose exec db psql artist postgres

test:
pipenv run docker-compose -f docker-compose-test.yml up --build --abort-on-container-exit
test: setup-virtual-display
docker compose -f docker-compose-test.yml up --build --abort-on-container-exit --force-recreate

clean:
find . -name '*~' -delete
Expand Down
1 change: 1 addition & 0 deletions api/Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ psycopg2-binary = "*"
google-cloud-storage = "*"
django-cors-headers = "*"
django-allauth = {extras = ["socialaccount"], version = "*"}
pillow = "*"

[dev-packages]

Expand Down
488 changes: 272 additions & 216 deletions api/Pipfile.lock

Large diffs are not rendered by default.

18 changes: 18 additions & 0 deletions api/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,26 @@ Set up your local development environment for the API.
- You can start by copying the example and updating the values. `cp .env.example .env`
- Or you can copy a `.env` file from another api in this project.
4. `make setup` will handle pipenv fun for you.
- Run this again if new packages were added with `pipenv install ...`
5. Do something to improve this getting started guide!

#### Local Django Admin

The app uses oauth and the Django admin can be accessed on a locally running server at [http://localhost:8000/admin/].

The first time your run, and any time you destroy your `docker-compose` volumes, you'll need to set yourself up as a super user.

1. [Sign in with OAuth](http://localhost:8000/accounts/google/login/?process=login). This creates an account that you will give staff and superuser access.
1. Jump onto the running psql container.
1. Find the container ID by running `docker ps` and copy the `Container ID` for the `postgres` image.
1. Run `docker exec it containerIdThatYouCopied bash` to connect to the running postgres container.
1. Run `psql artist -Upostgres` to access the `artist` database.
1. Run `select * from auth_user;` to see your user table. You should see exactly 1 user and it should be you.
1. Give yourself super admin permissions by running `update auth_user set is_staff = 't', is_superuser = 't' where id = 1;`. This assumes your user's row ID is 1. It should be.
1. Access the admin console at [http://localhost:8000/admin/].



## Run

```
Expand Down
15 changes: 15 additions & 0 deletions api/api/art_storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@

class ArtStorage:

def __new__(cls):
"""Create or return the singleton instance of the ArtStorage."""
if not hasattr(cls, 'instance'):
cls.instance = super(ArtStorage, cls).__new__(cls)
return cls.instance

def __init__(self):
self.gs = storage.Client()
self.bucket = self.gs.bucket(BUCKET_NAME)
Expand All @@ -26,3 +32,12 @@ def get_art(self, generation):
'artist_id': m['artist_id'],
})
return {'art': art}

def new_image_file(self, generation: int, artist_id: int):
blob = self.bucket.blob(f'gen-{generation}/{artist_id}.jpg')
#blob.acl.all().grant_read()
return blob

def open(self, blob):
"""Gets the context manager for a filepointer to write the art to."""
return blob.open(mode='wb', ignore_flush=True)
18 changes: 18 additions & 0 deletions api/api/migrations/0006_alter_artist_public_link.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 5.1.1 on 2024-09-15 17:47

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('api', '0005_remove_newvote_artist_remove_newvote_user_artist_and_more'),
]

operations = [
migrations.AlterField(
model_name='artist',
name='public_link',
field=models.URLField(null=True),
),
]
8 changes: 4 additions & 4 deletions api/api/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@


def get_current_generation():
# Get the max id of where `active_date` is not `Null`.
# Get the max id of where `active_date` is not `Null` or `None`.
gen = (Generation.objects
.filter(active_date__isnull=False)
.order_by("-id")
.values_list("id", flat=True)[0])
.values_list("id", flat=True).first())
return gen


Expand Down Expand Up @@ -38,13 +38,13 @@ class Generation(Datetime):
inaction_date = models.DateTimeField(null=True)

def __str__(self):
return f'{self.id}: {self.active_date} - {self.inaction_date}'
return f'Generation: {self.id}: {self.active_date} - {self.inaction_date}'


class Artist(Datetime):
id = models.AutoField(primary_key=True)
dna = models.TextField()
public_link = models.URLField(max_length=200)
public_link = models.URLField(max_length=200, null=True)
generation = models.ForeignKey(Generation, on_delete=models.CASCADE)


Expand Down
1 change: 1 addition & 0 deletions api/app/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
urlpatterns = [
# Local endpoints.
path("api/", include("api.urls")),
path("painter/", include("painter.urls")),

# Django built-ins
path("admin/", admin.site.urls),
Expand Down
6 changes: 4 additions & 2 deletions api/docker-compose-test.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
version: '3.9'

services:
db:
db-test:
image: postgres:15
restart: always
env_file:
Expand All @@ -14,10 +14,12 @@ services:
entrypoint: python manage.py test
volumes:
- ./:/home/appuser/
# x11 forwarding, to connect to the host x11 port.
- /tmp/.X11-unix:/tmp/.X11-unix
env_file:
- ./.env.test
depends_on:
- db
- db-test

volumes:
postgres_data:
2 changes: 2 additions & 0 deletions api/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ services:
volumes:
- ./:/home/appuser/
- ${GOOGLE_APPLICATION_CREDENTIALS}:${GOOGLE_APPLICATION_CREDENTIALS}
# x11 forwarding, to connect to the host x11 port.
- /tmp/.X11-unix:/tmp/.X11-unix
ports:
- 8000:8000
env_file:
Expand Down
File renamed without changes.
File renamed without changes.
56 changes: 56 additions & 0 deletions api/painter/generation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import random

from api import art_storage
from api import models
from painter.graphics import engine
from painter import painter as _painter


_GRAPHICS_ENGINE = None

def graphics_engine():
global _GRAPHICS_ENGINE
if not _GRAPHICS_ENGINE:
_GRAPHICS_ENGINE = engine.TurtleEngine()
return _GRAPHICS_ENGINE


# Hardcode a few special values because they're just used once.
_NUM_ARTISTS = 64
_NUM_CHROMOSOMES = 32
_MIN_CHROMOSOME_LENGTH = 256
_MAX_CHROMOSOME_LENGTH = 512


def bootstrap():
"""Create a first generation of artists.
"""
gen = models.Generation.objects.create()

for i in range(_NUM_ARTISTS):
# Build the DNA string of chromosomes.
chromosomes = []
for _ in range(_NUM_CHROMOSOMES):
chromo_len = random.randint(_MIN_CHROMOSOME_LENGTH, _MAX_CHROMOSOME_LENGTH-1)
chromo_str = ''.join(random.choices('ATCG', k=chromo_len))
chromosomes.append(chromo_str)

dna = '\n'.join(chromosomes)
artist = models.Artist.objects.create(
dna=dna,
generation=gen,
)
paint(artist, gen)


def paint(artist, gen):
"""Paints and saves this artist's masterpiece to the artist model.
"""
graphics_engine().reset()
p = _painter.Painter(artist.dna, graphics_engine())
while p.still_growing():
p.paint()
p.age_up()
public_url = graphics_engine().save_image(art_storage.ArtStorage(), gen.id, artist.id)
artist.public_url = public_url
artist.save()
File renamed without changes.
61 changes: 61 additions & 0 deletions api/painter/graphics/engine.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import abc
import io
import tempfile
import turtle

from PIL import Image

from painter.graphics import actions


class EngineInterface(metaclass=abc.ABCMeta):

def __init__(self, action_class):
self.action_class = action_class

def save_image(self, output_filename: str):
pass

def get_action(self, index):
return self.action_class(index)

def get_action_count(self):
return len(self.action_class.__members__)


class TurtleEngine(EngineInterface):

def __new__(cls):
"""Create or return the singleton instance of the ArtStorage."""
if not hasattr(cls, 'instance'):
cls.instance = super(TurtleEngine, cls).__new__(cls)
return cls.instance

def __init__(self):
EngineInterface.__init__(self, actions.TurtleAction)
self.reset()

def save_image(self, storage, generation: int, artist_id: int):
"""Save a JPEG image to the art_storage.
Args:
- storage: The object responsible for storaging images.
- generation: The generation number, used by `storage` to know where to save the image.
- artist_id: The artist's numeric ID, used by `storage` to know where to save the image.
Returns:
- str: The public URL to the newly saved image file.
"""
canvas = turtle.getscreen().getcanvas()
ps = canvas.postscript()
image_file = storage.new_image_file(generation, artist_id)
with Image.open(io.BytesIO(ps.encode('utf-8'))) as img:
with storage.open(image_file) as fp:
img.save(fp, format='JPEG')
return image_file.public_url

def reset(self):
turtle.clearscreen()
turtle.speed(10)
turtle.hideturtle()
turtle.getscreen().colormode(255)
File renamed without changes.
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
""" DEPRECCATED !!! DELETE ME DELETE ME"""

import os

from google.cloud import storage
Expand All @@ -11,7 +13,3 @@ def __init__(self):
self.gs = storage.Client()
self.bucket = self.gs.bucket(BUCKET_NAME)

def open(self, generation, artist_id):
"""Gets the context manager for a filepointer to write the art to."""
blob = self.bucket.blob(f'gen-{generation}/{artist_id}.jpg')
return blob.open(mode='wb', ignore_flush=True)
File renamed without changes.
File renamed without changes.
Empty file.
Loading

0 comments on commit 961d15d

Please sign in to comment.