Skip to content

Commit

Permalink
initial commit, MVP
Browse files Browse the repository at this point in the history
  • Loading branch information
bbilly1 committed Jun 13, 2022
0 parents commit d1b210b
Show file tree
Hide file tree
Showing 41 changed files with 1,929 additions and 0 deletions.
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
.vscode
__pycache__

# testing logos
custom_logos
43 changes: 43 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# build requirements
FROM python:3.10.5-slim-bullseye AS builder

RUN apt-get -y update && \
apt-get -y install --no-install-recommends \
build-essential

ENV PATH=/root/.local/bin:$PATH
COPY tilefy/requirements.txt /requirements.txt
RUN pip install --upgrade pip && pip install --user -r requirements.txt


# load in main image
FROM python:3.10.5-slim-bullseye as tilefy
ARG INSTALL_DEBUG
ENV PYTHONUNBUFFERED 1

# copy build requirements
COPY --from=builder /root/.local /root/.local
ENV PATH=/root/.local/bin:$PATH

RUN apt-get clean && apt-get -y update && \
apt-get -y install --no-install-recommends \
ttf-bitstream-vera fonts-liberation && \
rm -rf /var/lib/apt/lists/*

# install debug tools for testing environment
RUN if [ "$INSTALL_DEBUG" ] ; then \
apt-get -y update && apt-get -y install --no-install-recommends \
vim htop bmon net-tools iputils-ping procps \
&& pip install --user ipython \
; fi

RUN mkdir /data
RUN mkdir /app

COPY tilefy /app
WORKDIR /app

RUN chmod +x ./start.sh

VOLUME /data
CMD ["./start.sh"]
674 changes: 674 additions & 0 deletions LICENSE

Large diffs are not rendered by default.

139 changes: 139 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
![Tilefy](assets/tilefy-banner.jpg?raw=true "Tilefy Banner")

<center><h1>Create beautiful tiles for your project</h1></center>

## Table of contents
- [Core functionality](#core-functionality)
- [Screenshots](#screenshots)
- [Installation](#installation)
- [Configuration](#configuration)
- [API requests](#api-requests)
- [Plugins](#plugins)
- [Donate](#donate)

## Core functionality
- Dynamically create and recreate PNG tiles
- Showcase any project stats accessible over a public API
- Customize to your liking with your branding and color scheme
- Embed your tiles anywhere you can embed a image
- Self Hosted with Docker

## Screenshots
![home screenshot](assets/screenshot.png?raw=true "Tilefy Home Page")

## Installation
Take a look at the example `docker-compose.yml` file provided. Tilefy depends on two containers:

### Tilefy
Main Python application to create and serve your tiles, built with Flask.
- Serves the interface on port `8000`
- Needs a volume at **/data** to store your tiles, custom fonts and logos and your **tiles.yml** config file.
- Set your Redis connection with the environment variables `REDIS_HOST` and `REDIS_PORT`.
- Set the environment variable `TILEFY_HOST` to your full url from where you are hosting this application. Needed to build links and templates to embed your tiles. Don't add a trailing `/`.
- Set your timezone with the `TZ` environment variable to configure the scheduler, defaults to *UTC*.

### Redis JSON
Functions as a cache and holds the scheduler data storage and history.
- Needs a volume at **/data** to store your configurations permanently.

## Configuration
Create a yml config file where you have mounted your `/data/tiles.yml` folder. Take a look at the provided `tiles.example.yml` for the basic syntax. *tiles* is the top level key, list your tiles below. The main key of the tile is your slug and will become your url, so use no spaces or special characters.

### tile_name
Give your tile a unique human readable name.

### background_color, font_color
Hex color code for background and font, make sure to add the *""* to escape the *#* symbol.

### width, height
Size of the tile in pixels.

### logos
List of logos, get a list of pre installed logos:
```bash
docker exec -it tilefy ls logos
```
Add the pre installed logos by providing the file name. Contribute by adding additional commonly used logos.

Provide your custom logos by adding them to `/data/logos/`, in PNG file format with transparent background. Add your custom logos by providing a relative path like `logos/file-name.png`.

### font: optional
Font defaults to *Vera.ttf*. Use a pre installed font from the *liberation* or *ttf-bitstream-vera* packages by providing the relative path from the truetype folder.
List available pre installed fonts:
```bash
docker exec -it tilefy ls /usr/share/fonts/truetype/liberation
docker exec -it tilefy ls /usr/share/fonts/truetype/ttf-bitstream-vera
```

Provide your custom font by adding them to `/data/fonts`, in TTF format only and add them with their relative path like `fonts/font-name.ttf`.

### humanize: optional
Defaults to `true` for all numbers. Shorten long numbers in to a more human readable string, like *14502* to *14.5K*.

### recreate: optional
Recreate tiles periodically, provide your custom schedule as a cron tab or use `on_demand` to recreate the tile for every request. Defaults to `0 0 * * *` aka every day at midnight. Be aware of any rate limiting and API quotas you might face with a too frequent schedule.
Note:
- There is automatically a random jitter for cron tab of 15 secs to avoid parallel requests for a lot of tiles.
- There is a failsafe in place to block recreating tiles faster than every 60 seconds.

## API requests
Get values from a public API by providing the url and key_map.

### url
JSON API endpoint. If you can, filter and reduce the API response size by requesting only the required fields.

### key_map
Navigate the JSON object to find your desired value. Each item in the list navigates one level deeper, a `string` accesses a key and a `int` accesses an index in an array.

*Example 1*
```json
{
"pull_count": 269912,
"star_count": 11,
}
```
To for example access the *pull_count* key in the top level of the response:
```yml
key_map:
- pull_count
```
*Example 2*
```json
{
"results": [
{
"status": "success",
"last_run": "timestamp",
}
]
}
```

To for example access the *status* key in the first dictionary of the results list:
```yml
key_map:
- results
- 0
- status
```
## Plugins
For all values not accessible over a JSON API endpoint, this will need a dedicated solution by for example scraping the website. This is inherently less reliable as websites change more frequently than APIs.
### Chrome extension users
Get the amount of active chrome extension users.
```yml
plugin:
name: chrome-extension-users
id: jjnkmicfnfojkkgobdfeieblocadmcie
```
Please contribute and open feature requests to add more.
## Donate
The best donation to **Tilefy** is your time, contribute in any way you can to improve this project.
Second best way to support the development is to provide for caffeinated beverages:
* [Paypal.me](https://paypal.me/bbilly1) for a one time coffee
* [Paypal Subscription](https://www.paypal.com/webapps/billing/plans/subscribe?plan_id=P-03770005GR991451KMFGVPMQ) for a monthly coffee
* [ko-fi.com](https://ko-fi.com/bbilly1) for an alternative platform
Binary file added assets/screenshot.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/tilefy-banner.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/tilefy-banner.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/tilefy-logo-only.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/tilefy-logo-only.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/tilefy-logo-text.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/tilefy-logo-text.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/tilefy-social-preview.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
95 changes: 95 additions & 0 deletions deploy.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
#!/bin/bash
# deploy app

test_host="tilefy.local"

function rebuild_test {
echo "rebuild testing environment"
rsync -a --progress --delete-after \
--exclude ".git" \
--exclude ".gitignore" \
--exclude "**/cache" \
--exclude "**/__pycache__/" \
--exclude "db.sqlite3" \
. -e ssh "$test_host":tilefy

rsync --progress --ignore-existing docker-compose.yml -e ssh "$test_host":docker
rsync --progress tiles.example.yml -e ssh "$test_host":docker/volume/tilefy/data/tiles.yml

ssh "$test_host" "docker buildx build --build-arg INSTALL_DEBUG=1 -t bbilly1/tilefy tilefy --load"
ssh "$test_host" 'docker compose -f docker/docker-compose.yml up -d --build'
}

function validate {

if [[ $1 ]]; then
check_path="$1"
else
check_path="."
fi

echo "run validate on $check_path"

echo "running black"
black --diff --color --check -l 79 "$check_path"
echo "running codespell"
codespell --skip="./.git" "$check_path"
echo "running flake8"
flake8 "$check_path" --count --max-complexity=10 --max-line-length=79 \
--show-source --statistics
echo "running isort"
isort --check-only --diff --profile black -l 79 "$check_path"
printf " \n> all validations passed\n"

}

function docker_publish {

# check things
if [[ $(git branch --show-current) != 'master' ]]; then
echo 'you are not on master, dummy!'
return
fi

if [[ $(systemctl is-active docker) != 'active' ]]; then
echo "starting docker"
sudo systemctl start docker
fi
echo "latest tags:"
git tag | tail -n 5 | sort -r

printf "\ncreate new version:\n"
read -r VERSION

echo "build and push $VERSION?"
read -rn 1

# start build
sudo docker buildx build \
--platform linux/amd64,linux/arm64 \
-t bbilly1/tilefy \
-t bbilly1/tilefy:"$VERSION" --push .

# create release tag
echo "commits since last version:"
git log "$(git describe --tags --abbrev=0)"..HEAD --oneline
git tag -a "$VERSION" -m "new release version $VERSION"
git push origin "$VERSION"

}


if [[ $1 == "test" ]]; then
rebuild_test
elif [[ $1 == "validate" ]]; then
# check package versions in requirements.txt for updates
python version_check.py
validate "$2"
elif [[ $1 == "docker" ]]; then
docker_publish
else
echo "valid options are: test | docker "
fi

##
exit 0
24 changes: 24 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
version: '3.3'

services:
tilefy:
container_name: tilefy
restart: always
image: bbilly1/tilefy
volumes:
- ./volume/tilefy/data:/data
ports:
- 8000:8000
environment:
- TZ=America/New_York
- TILEFY_HOST=http://tilefy.local
- REDIS_HOST=tilefy-redis
- REDIS_PORT=6379
tilefy-redis:
image: redislabs/rejson
container_name: tilefy-redis
restart: always
expose:
- "6379"
volumes:
- ./volume/tilefy/redis:/data
Binary file added tilefy/logos/docker.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added tilefy/logos/firefox.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added tilefy/logos/github-actions.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added tilefy/logos/github-star.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added tilefy/logos/google-chrome.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 8 additions & 0 deletions tilefy/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
APScheduler==3.9.1
beautifulsoup4==4.11.1
flask==2.1.2
Pillow==9.1.1
PyYAML==6.0
redis==4.3.3
requests==2.28.0
uwsgi==2.0.20
Empty file added tilefy/src/__init__.py
Empty file.
78 changes: 78 additions & 0 deletions tilefy/src/api_call.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
"""make api call to get variable text"""

import requests
from src.plugins.chrome_extension import ChromeExtension


def humanize(number):
"""humanize number to string"""

break_points = [
(999999999, "B"),
(999999, "M"),
(999, "K"),
]

for break_point in break_points:
break_nr, break_str = break_point
if number > break_nr:
return f"{round(number / (break_nr + 1), 1)}{break_str}"

return str(number)


class Api:
"""make request and parse"""

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

def get_text(self):
"""run all"""
if "plugin" in self.tile_config:
key_match = self.plugin()
else:
key_match = self.url()

to_humanize = self.tile_config.get("humanize", True)
if not to_humanize or isinstance(key_match, str):
return str(key_match)

return humanize(key_match)

def url(self):
"""make call using url from tile_config"""
response = self.make_request()
key_match = self.walk_response(response)
if not key_match:
print(f"failed to result with key_map: {response}")
raise ValueError

return key_match

def make_request(self):
"""make request"""
url = self.tile_config["url"]
response = requests.get(url, timeout=1)
if not response.ok:
print(f"failed to make request: {url}")
raise ValueError

return response.json()

def walk_response(self, response):
"""walk response dict for key"""
for item in self.tile_config["key_map"]:
response = response[item]

return response

def plugin(self):
"""make request with pugin"""
plugin_name = self.tile_config["plugin"]["name"]
item_id = self.tile_config["plugin"]["id"]
if plugin_name == "chrome-extension-users":
users = ChromeExtension(item_id).get()
return users

raise ValueError("missing plugin")
Empty file added tilefy/src/plugins/__init__.py
Empty file.
Loading

0 comments on commit d1b210b

Please sign in to comment.