Skip to content

Commit

Permalink
feat: introducing pokemon container for a friend
Browse files Browse the repository at this point in the history
  • Loading branch information
aumer-amr committed Jun 7, 2024
1 parent e2b37d2 commit de96708
Show file tree
Hide file tree
Showing 9 changed files with 576 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .github/workflows/action-build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ jobs:
- dockerfile: ./apps/bitwarden-backup/Dockerfile
base: ./apps/bitwarden-backup
image: ghcr.io/aumer-amr/bitwarden-backup
- dockerfile: ./apps/pokemon/Dockerfile
base: ./apps/pokemon
image: ghcr.io/aumer-amr/pokemon
permissions:
contents: read
packages: write
Expand Down
17 changes: 17 additions & 0 deletions apps/pokemon/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Use an official Python runtime as a base image
FROM python:3.8-slim

# Set the working directory in the container
WORKDIR /app

# Copy the current directory contents into the container at /app
COPY . /app

# Install any needed packages specified in requirements.txt
RUN pip install --no-cache-dir -r requirements.txt

# Make port 5000 available to the world outside this container
EXPOSE 5000

# Run app.py when the container launches
CMD ["python", "app.py"]
50 changes: 50 additions & 0 deletions apps/pokemon/PokemonTypeEffectiveness.spec
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# PokemonTypeEffectiveness.spec

# -*- mode: python ; coding: utf-8 -*-

block_cipher = None

a = Analysis(
['app.py'],
pathex=['path_to_your_project'],
binaries=[],
datas=[
('templates', 'templates'),
('pokemon_cache.json', '.'),
('processed_pokemon_cache.json', '.')
],
hiddenimports=[],
hookspath=[],
runtime_hooks=[],
excludes=[],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=block_cipher,
)
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
exe = EXE(
pyz,
a.scripts,
[],
exclude_binaries=True,
name='PokemonTypeEffectiveness',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
console=True,
disable_windowed_traceback=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None
)
coll = COLLECT(
exe,
a.binaries,
a.zipfiles,
a.datas,
strip=False,
upx=True,
upx_exclude=[],
name='PokemonTypeEffectiveness',
)
226 changes: 226 additions & 0 deletions apps/pokemon/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
from flask import Flask, request, jsonify, render_template
import aiohttp
import asyncio
import difflib
import os
import json
import ssl
import certifi
import logging
from aiohttp import ClientTimeout

# Set up logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger()

app = Flask(__name__, template_folder='templates')

CACHE_FILE = os.path.join(os.path.dirname(__file__), 'data', 'pokemon_cache.json')
PROCESSED_CACHE_FILE = os.path.join(os.path.dirname(__file__), 'data', 'processed_pokemon_cache.json')
REQUEST_TIMEOUT = 10 # seconds

async def create_aiohttp_session():
ssl_context = ssl.create_default_context(cafile=certifi.where())
timeout = ClientTimeout(total=REQUEST_TIMEOUT)
return aiohttp.ClientSession(connector=aiohttp.TCPConnector(ssl=ssl_context), timeout=timeout)

async def fetch(session, url):
try:
async with session.get(url) as response:
if response.status != 200:
logger.error(f"Failed to fetch {url}: {response.status}")
return None
return await response.json()
except asyncio.TimeoutError:
logger.error(f"Request to {url} timed out.")
return None
except aiohttp.ClientError as e:
logger.error(f"Client error occurred while fetching {url}: {e}")
return None

async def get_all_pokemon(session):
url = "https://pokeapi.co/api/v2/pokemon?limit=10000"
return await fetch(session, url)

async def cache_pokemon_data():
async with await create_aiohttp_session() as session:
logger.info("Fetching Pokémon data from PokeAPI...")
pokemon_data = await get_all_pokemon(session)
if pokemon_data:
with open(CACHE_FILE, 'w') as f:
json.dump(pokemon_data, f)
logger.info("Pokemon data cached successfully.")
await process_and_cache_pokemon_data(session, pokemon_data)

async def load_cached_pokemon_data():
if os.path.exists(CACHE_FILE):
logger.info("Loading cached Pokémon data...")
with open(CACHE_FILE, 'r') as f:
return json.load(f)
else:
await cache_pokemon_data()
return await load_cached_pokemon_data()

async def check_for_updates():
async with await create_aiohttp_session() as session:
logger.info("Checking for updates...")
cached_data = await load_cached_pokemon_data()
current_data = await get_all_pokemon(session)
if current_data['count'] != cached_data['count']:
logger.info("Updates found, updating cache...")
await cache_pokemon_data()
logger.info("Pokemon data updated.")
else:
logger.info("No updates found.")
if not os.path.exists(PROCESSED_CACHE_FILE):
logger.info("Processed cache not found, processing data...")
await process_and_cache_pokemon_data(session, cached_data)

async def process_and_cache_pokemon_data(session, pokemon_data):
processed_data = []
logger.info("Processing Pokémon data...")
for i, pokemon in enumerate(pokemon_data['results']):
logger.info(f"Processing Pokémon {i+1}/{len(pokemon_data['results'])}: {pokemon['name']}")
pokemon_details = await fetch(session, pokemon['url'])
if not pokemon_details:
logger.error(f"Failed to fetch details for {pokemon['name']}")
continue

species_url = pokemon_details['species']['url']
species_data = await fetch(session, species_url)
varieties = species_data.get('varieties', []) if species_data else []

for variety in varieties:
variety_url = variety['pokemon']['url']
variety_data = await fetch(session, variety_url)
if not variety_data:
logger.error(f"Failed to fetch variety details for {pokemon['name']}")
continue

types = variety_data['types']
type_names = [type_info['type']['name'] for type_info in types]

type_urls = [type_info['type']['url'] for type_info in types]
type_data_list = await asyncio.gather(*[fetch(session, url) for url in type_urls])
type_data_list = [data for data in type_data_list if data]
effectiveness = calculate_type_effectiveness(type_data_list)

processed_data.append({
'name': variety_data['name'],
'display_name': variety['pokemon']['name'].capitalize(), # Corrected display_name assignment
'form': variety['pokemon']['name'],
'id': variety_data['id'],
'types': type_names,
'effectiveness': effectiveness
})

with open(PROCESSED_CACHE_FILE, 'w') as f:
json.dump(processed_data, f)
logger.info("Processed Pokémon data cached successfully.")

@app.route('/')
def index():
return render_template('Pokemon Type Effectiveness.html')

@app.route('/api/pokemon', methods=['GET'])
async def get_pokemon_info():
try:
pokemon_name = request.args.get('name')
if not pokemon_name:
return jsonify({'error': 'No Pokémon name provided'}), 400

# Ensure processed cache is loaded
if not os.path.exists(PROCESSED_CACHE_FILE):
logger.info("Processed cache not found, processing data...")
await load_cached_pokemon_data()
await check_for_updates()

with open(PROCESSED_CACHE_FILE, 'r') as f:
processed_data = json.load(f)

matches = [pokemon for pokemon in processed_data if pokemon_name.lower() in pokemon['name'].lower()]

if not matches:
closest_matches = difflib.get_close_matches(pokemon_name.lower(), [p['name'] for p in processed_data])
if closest_matches:
suggestions = ', '.join([f'<a href="#" onclick="suggestPokemon(\'{match}\')">{match.capitalize()}</a>' for match in closest_matches])
return jsonify({'error': f'Pokémon not found. Did you mean: {suggestions}'}), 404
else:
return jsonify({'error': 'Pokémon not found and no close matches available'}), 404

# Debugging: Log matches to ensure correct data is being returned
for match in matches:
logger.info(f"Found match: {match}")

return jsonify(matches)

except Exception as e:
logger.error(f"Error occurred: {e}", exc_info=True)
return jsonify({'error': 'An error occurred while processing your request.'}), 500

def calculate_type_effectiveness(type_data_list):
damage_multipliers = {}

for type_data in type_data_list:
damage_relations = type_data['damage_relations']

for relation_type, related_types in damage_relations.items():
multiplier = 1
if relation_type == 'double_damage_from':
multiplier = 2
elif relation_type == 'half_damage_from':
multiplier = 0.5
elif relation_type == 'no_damage_from':
multiplier = 0

for related_type in related_types:
type_name = related_type['name']
if type_name not in damage_multipliers:
damage_multipliers[type_name] = 1
damage_multipliers[type_name] *= multiplier

effectiveness = {
'four_times_effective': [],
'super_effective': [],
'normal_effective': [],
'resistant': [],
'immune': []
}

for type_name, multiplier in damage_multipliers.items():
if multiplier == 4:
effectiveness['four_times_effective'].append(type_name)
elif multiplier == 2:
effectiveness['super_effective'].append(type_name)
elif multiplier == 1:
effectiveness['normal_effective'].append(type_name)
elif multiplier == 0.5:
effectiveness['resistant'].append(type_name)
elif multiplier == 0:
effectiveness['immune'].append(type_name)

all_types = set([
'normal', 'fire', 'water', 'electric', 'grass', 'ice', 'fighting', 'poison',
'ground', 'flying', 'psychic', 'bug', 'rock', 'ghost', 'dragon', 'dark', 'steel', 'fairy'
])
categorized_types = (
set(effectiveness['four_times_effective']) |
set(effectiveness['super_effective']) |
set(effectiveness['resistant']) |
set(effectiveness['immune'])
)
effectiveness['normal_effective'] = list(all_types - categorized_types)

return effectiveness



if __name__ == '__main__':
try:
logger.info("Starting initialization...")
asyncio.run(check_for_updates()) # Ensure cache is updated on startup
logger.info("Starting Flask server...")
app.run(host='0.0.0.0', port=5000)
except Exception as e:
logger.error(f"Fatal error occurred: {e}", exc_info=True)
input("Press Enter to exit...") # Keep the console window open
38 changes: 38 additions & 0 deletions apps/pokemon/app.spec
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# -*- mode: python ; coding: utf-8 -*-


a = Analysis(
['app.py'],
pathex=[],
binaries=[],
datas=[],
hiddenimports=[],
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=[],
noarchive=False,
optimize=0,
)
pyz = PYZ(a.pure)

exe = EXE(
pyz,
a.scripts,
a.binaries,
a.datas,
[],
name='app',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
upx_exclude=[],
runtime_tmpdir=None,
console=True,
disable_windowed_traceback=False,
argv_emulation=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None,
)
1 change: 1 addition & 0 deletions apps/pokemon/data/pokemon_cache.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions apps/pokemon/readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Just hosting for a friend smile :)
29 changes: 29 additions & 0 deletions apps/pokemon/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
aiohttp==3.9.3
aiosignal==1.3.1
altgraph==0.17.4
asgiref==3.8.1
async-timeout==4.0.3
attrs==23.2.0
blinker==1.8.2
certifi==2024.6.2
charset-normalizer==3.3.2
click==8.1.7
colorama==0.4.6
discord.py==2.3.2
Flask==3.0.3
frozenlist==1.4.1
idna==3.6
itsdangerous==2.2.0
Jinja2==3.1.4
MarkupSafe==2.1.5
multidict==6.0.5
packaging==24.0
pefile==2023.2.7
pyinstaller==6.7.0
pyinstaller-hooks-contrib==2024.6
pywin32-ctypes==0.2.2
requests==2.32.3
typing_extensions==4.12.1
urllib3==2.2.1
Werkzeug==3.0.3
yarl==1.9.4
Loading

0 comments on commit de96708

Please sign in to comment.