Skip to content

Commit

Permalink
Add function to bootstrap the first generation.
Browse files Browse the repository at this point in the history
  • Loading branch information
anybodys committed Sep 22, 2024
1 parent 2a2bbc7 commit 25d71e3
Show file tree
Hide file tree
Showing 34 changed files with 560 additions and 390 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,18 @@ name: CI Painter 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
11 changes: 7 additions & 4 deletions api/Makefile
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
.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:
pipenv run Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 &

run: run-api

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

db-migrate:
Expand All @@ -18,8 +21,8 @@ db-makemigrations:
db-shell:
pipenv run docker-compose exec db psql artist local-user

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

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.
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from painter.graphics import engine


engine = engine.TurtleEngine(None)
engine = engine.TurtleEngine()

class TestTurtleAction(unittest.TestCase):

Expand Down
Loading

0 comments on commit 25d71e3

Please sign in to comment.