Update Exploring Near Earth Objects project and add Meme Generator project
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -174,3 +174,6 @@ cython_debug/
|
||||
# PyPI configuration file
|
||||
.pypirc
|
||||
|
||||
|
||||
# VS Code
|
||||
.vscode/
|
||||
@@ -21,6 +21,7 @@ class NEODatabase:
|
||||
help fetch NEOs by primary designation or by name and to help speed up
|
||||
querying for close approaches that match criteria.
|
||||
"""
|
||||
|
||||
def __init__(self, neos, approaches):
|
||||
"""Create a new `NEODatabase`.
|
||||
|
||||
@@ -42,9 +43,16 @@ class NEODatabase:
|
||||
self._neos = neos
|
||||
self._approaches = approaches
|
||||
|
||||
# TODO: What additional auxiliary data structures will be useful?
|
||||
self._neo_by_designation = {neo.designation: neo for neo in self._neos}
|
||||
self._neo_by_name = {neo.name: neo for neo in self._neos}
|
||||
|
||||
# TODO: Link together the NEOs and their close approaches.
|
||||
for approach in self._approaches:
|
||||
# Link approach.neo to the corresponding NearEarthObject, namely
|
||||
# the one whose designation matches approach._designation.
|
||||
approach.neo = self._neo_by_designation[approach._designation]
|
||||
# Add this approach to the corresponding NearEarthObject's
|
||||
# .approaches collection.
|
||||
self._neo_by_designation[approach._designation].approaches.append(approach)
|
||||
|
||||
def get_neo_by_designation(self, designation):
|
||||
"""Find and return an NEO by its primary designation.
|
||||
@@ -59,8 +67,7 @@ class NEODatabase:
|
||||
:param designation: The primary designation of the NEO to search for.
|
||||
:return: The `NearEarthObject` with the desired primary designation, or `None`.
|
||||
"""
|
||||
# TODO: Fetch an NEO by its primary designation.
|
||||
return None
|
||||
return self._neo_by_designation.get(designation, None)
|
||||
|
||||
def get_neo_by_name(self, name):
|
||||
"""Find and return an NEO by its name.
|
||||
@@ -76,8 +83,7 @@ class NEODatabase:
|
||||
:param name: The name, as a string, of the NEO to search for.
|
||||
:return: The `NearEarthObject` with the desired name, or `None`.
|
||||
"""
|
||||
# TODO: Fetch an NEO by its name.
|
||||
return None
|
||||
return self._neo_by_name.get(name, None)
|
||||
|
||||
def query(self, filters=()):
|
||||
"""Query close approaches to generate those that match a collection of filters.
|
||||
@@ -93,6 +99,7 @@ class NEODatabase:
|
||||
:param filters: A collection of filters capturing user-specified criteria.
|
||||
:return: A stream of matching `CloseApproach` objects.
|
||||
"""
|
||||
# TODO: Generate `CloseApproach` objects that match all of the filters.
|
||||
for approach in self._approaches:
|
||||
cases = [filter_i(approach) for filter_i in filters]
|
||||
if all(cases):
|
||||
yield approach
|
||||
|
||||
@@ -12,9 +12,9 @@ line, and uses the resulting collections to build an `NEODatabase`.
|
||||
|
||||
You'll edit this file in Task 2.
|
||||
"""
|
||||
|
||||
import csv
|
||||
import json
|
||||
|
||||
from models import NearEarthObject, CloseApproach
|
||||
|
||||
|
||||
@@ -24,8 +24,16 @@ def load_neos(neo_csv_path):
|
||||
:param neo_csv_path: A path to a CSV file containing data about near-Earth objects.
|
||||
:return: A collection of `NearEarthObject`s.
|
||||
"""
|
||||
# TODO: Load NEO data from the given CSV file.
|
||||
return ()
|
||||
neos = []
|
||||
with open(neo_csv_path, "r") as csv_file:
|
||||
reader = csv.reader(csv_file)
|
||||
next(reader)
|
||||
for row in reader:
|
||||
neo = NearEarthObject(
|
||||
designation=row[3], name=row[4], diameter=row[15], hazardous=row[7]
|
||||
)
|
||||
neos.append(neo)
|
||||
return neos
|
||||
|
||||
|
||||
def load_approaches(cad_json_path):
|
||||
@@ -34,5 +42,17 @@ def load_approaches(cad_json_path):
|
||||
:param cad_json_path: A path to a JSON file containing data about close approaches.
|
||||
:return: A collection of `CloseApproach`es.
|
||||
"""
|
||||
# TODO: Load close approach data from the given JSON file.
|
||||
return ()
|
||||
approaches = []
|
||||
with open(cad_json_path, "r") as json_file:
|
||||
cad = json.load(json_file)
|
||||
for item in cad["data"]:
|
||||
# 0: pdes, 3: cd, 4: dist, 7: v_rel
|
||||
approach = CloseApproach(
|
||||
designation=item[0],
|
||||
time=item[3],
|
||||
distance=float(item[4]),
|
||||
velocity=float(item[7]),
|
||||
)
|
||||
|
||||
approaches.append(approach)
|
||||
return approaches
|
||||
|
||||
@@ -16,7 +16,9 @@ iterator.
|
||||
|
||||
You'll edit this file in Tasks 3a and 3c.
|
||||
"""
|
||||
|
||||
import operator
|
||||
import itertools
|
||||
|
||||
|
||||
class UnsupportedCriterionError(NotImplementedError):
|
||||
@@ -38,6 +40,7 @@ class AttributeFilter:
|
||||
Concrete subclasses can override the `get` classmethod to provide custom
|
||||
behavior to fetch a desired attribute from the given `CloseApproach`.
|
||||
"""
|
||||
|
||||
def __init__(self, op, value):
|
||||
"""Construct a new `AttributeFilter` from an binary predicate and a reference value.
|
||||
|
||||
@@ -72,12 +75,57 @@ class AttributeFilter:
|
||||
return f"{self.__class__.__name__}(op=operator.{self.op.__name__}, value={self.value})"
|
||||
|
||||
|
||||
class DistanceFilter(AttributeFilter):
|
||||
"""Subclass of AttributeFilter to filter by distance attribute."""
|
||||
|
||||
@classmethod
|
||||
def get(cls, approach):
|
||||
return approach.distance
|
||||
|
||||
|
||||
class VelocityFilter(AttributeFilter):
|
||||
"""Subclass of AttributeFilter to filter by velocity attribute."""
|
||||
|
||||
@classmethod
|
||||
def get(cls, approach):
|
||||
return approach.velocity
|
||||
|
||||
|
||||
class DateFilter(AttributeFilter):
|
||||
"""Subclass of AttributeFilter to filter by date attribute."""
|
||||
|
||||
@classmethod
|
||||
def get(cls, approach):
|
||||
return approach.time.date()
|
||||
|
||||
|
||||
class DiameterFilter(AttributeFilter):
|
||||
"""Subclass of AttributeFilter to filter by diameter attribute."""
|
||||
|
||||
@classmethod
|
||||
def get(cls, approach):
|
||||
return approach.neo.diameter
|
||||
|
||||
|
||||
class HazardousFilter(AttributeFilter):
|
||||
"""Subclass of AttributeFilter to filter by hazardous attribute."""
|
||||
|
||||
@classmethod
|
||||
def get(cls, approach):
|
||||
return approach.neo.hazardous
|
||||
|
||||
|
||||
def create_filters(
|
||||
date=None, start_date=None, end_date=None,
|
||||
distance_min=None, distance_max=None,
|
||||
velocity_min=None, velocity_max=None,
|
||||
diameter_min=None, diameter_max=None,
|
||||
hazardous=None
|
||||
date=None,
|
||||
start_date=None,
|
||||
end_date=None,
|
||||
distance_min=None,
|
||||
distance_max=None,
|
||||
velocity_min=None,
|
||||
velocity_max=None,
|
||||
diameter_min=None,
|
||||
diameter_max=None,
|
||||
hazardous=None,
|
||||
):
|
||||
"""Create a collection of filters from user-specified criteria.
|
||||
|
||||
@@ -108,8 +156,30 @@ def create_filters(
|
||||
:param hazardous: Whether the NEO of a matching `CloseApproach` is potentially hazardous.
|
||||
:return: A collection of filters for use with `query`.
|
||||
"""
|
||||
# TODO: Decide how you will represent your filters.
|
||||
return ()
|
||||
filters = []
|
||||
|
||||
if date:
|
||||
filters.append(DateFilter(operator.eq, date))
|
||||
if start_date:
|
||||
filters.append(DateFilter(operator.ge, start_date))
|
||||
if end_date:
|
||||
filters.append(DateFilter(operator.le, end_date))
|
||||
if distance_min:
|
||||
filters.append(DistanceFilter(operator.ge, distance_min))
|
||||
if distance_max:
|
||||
filters.append(DistanceFilter(operator.le, distance_max))
|
||||
if velocity_min:
|
||||
filters.append(VelocityFilter(operator.ge, velocity_min))
|
||||
if velocity_max:
|
||||
filters.append(VelocityFilter(operator.le, velocity_max))
|
||||
if diameter_min:
|
||||
filters.append(DiameterFilter(operator.ge, diameter_min))
|
||||
if diameter_max:
|
||||
filters.append(DiameterFilter(operator.le, diameter_max))
|
||||
if hazardous is not None:
|
||||
filters.append(HazardousFilter(operator.eq, hazardous))
|
||||
|
||||
return filters
|
||||
|
||||
|
||||
def limit(iterator, n=None):
|
||||
@@ -121,5 +191,4 @@ def limit(iterator, n=None):
|
||||
:param n: The maximum number of values to produce.
|
||||
:yield: The first (at most) `n` values from the iterator.
|
||||
"""
|
||||
# TODO: Produce at most `n` values from the given iterator.
|
||||
return iterator
|
||||
return itertools.islice(iterator, n) if n else iterator
|
||||
|
||||
@@ -17,7 +17,10 @@ quirks of the data set, such as missing names and unknown diameters.
|
||||
|
||||
You'll edit this file in Task 1.
|
||||
"""
|
||||
|
||||
from helpers import cd_to_datetime, datetime_to_str
|
||||
import math
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class NearEarthObject:
|
||||
@@ -32,22 +35,20 @@ class NearEarthObject:
|
||||
initialized to an empty collection, but eventually populated in the
|
||||
`NEODatabase` constructor.
|
||||
"""
|
||||
# TODO: How can you, and should you, change the arguments to this constructor?
|
||||
# If you make changes, be sure to update the comments in this file.
|
||||
|
||||
def __init__(self, **info):
|
||||
"""Create a new `NearEarthObject`.
|
||||
|
||||
:param info: A dictionary of excess keyword arguments supplied to the constructor.
|
||||
"""
|
||||
# TODO: Assign information from the arguments passed to the constructor
|
||||
# onto attributes named `designation`, `name`, `diameter`, and `hazardous`.
|
||||
# You should coerce these values to their appropriate data type and
|
||||
# handle any edge cases, such as a empty name being represented by `None`
|
||||
# and a missing diameter being represented by `float('nan')`.
|
||||
self.designation = ''
|
||||
self.name = None
|
||||
self.diameter = float('nan')
|
||||
self.hazardous = False
|
||||
self.designation = info.get("designation")
|
||||
self.name = None if info.get("name") == "" else info.get("name")
|
||||
self.diameter = (
|
||||
float("nan")
|
||||
if info.get("diameter") in (None, "")
|
||||
else float(info.get("diameter"))
|
||||
)
|
||||
self.hazardous = info.get("hazardous", "N") == "Y"
|
||||
|
||||
# Create an empty initial collection of linked approaches.
|
||||
self.approaches = []
|
||||
@@ -55,20 +56,36 @@ class NearEarthObject:
|
||||
@property
|
||||
def fullname(self):
|
||||
"""Return a representation of the full name of this NEO."""
|
||||
# TODO: Use self.designation and self.name to build a fullname for this object.
|
||||
return ''
|
||||
if self.name:
|
||||
return f"{self.designation} {self.name}"
|
||||
return f"{self.designation}"
|
||||
|
||||
def __str__(self):
|
||||
"""Return `str(self)`."""
|
||||
# TODO: Use this object's attributes to return a human-readable string representation.
|
||||
# The project instructions include one possibility. Peek at the __repr__
|
||||
# method for examples of advanced string formatting.
|
||||
return f"A NearEarthObject ..."
|
||||
hazardous_str = (
|
||||
"is potentially hazardous"
|
||||
if self.hazardous
|
||||
else "is not potentially hazardous"
|
||||
)
|
||||
if not math.isnan(self.diameter):
|
||||
return f"NEO {self.fullname} has a diameter of {self.diameter:.3f} and {hazardous_str}."
|
||||
return f"NEO {self.fullname} has an unknown diameter and {hazardous_str}."
|
||||
|
||||
def __repr__(self):
|
||||
"""Return `repr(self)`, a computer-readable string representation of this object."""
|
||||
return f"NearEarthObject(designation={self.designation!r}, name={self.name!r}, " \
|
||||
return (
|
||||
f"NearEarthObject(designation={self.designation!r}, name={self.name!r}, "
|
||||
f"diameter={self.diameter:.3f}, hazardous={self.hazardous!r})"
|
||||
)
|
||||
|
||||
def serialize(self):
|
||||
"""Serialize this `NearEarthObject` into a dictionary."""
|
||||
return {
|
||||
"designation": self.designation,
|
||||
"name": self.name,
|
||||
"diameter_km": self.diameter,
|
||||
"potentially_hazardous": self.hazardous,
|
||||
}
|
||||
|
||||
|
||||
class CloseApproach:
|
||||
@@ -84,21 +101,20 @@ class CloseApproach:
|
||||
private attribute, but the referenced NEO is eventually replaced in the
|
||||
`NEODatabase` constructor.
|
||||
"""
|
||||
# TODO: How can you, and should you, change the arguments to this constructor?
|
||||
# If you make changes, be sure to update the comments in this file.
|
||||
|
||||
def __init__(self, **info):
|
||||
"""Create a new `CloseApproach`.
|
||||
|
||||
:param info: A dictionary of excess keyword arguments supplied to the constructor.
|
||||
"""
|
||||
# TODO: Assign information from the arguments passed to the constructor
|
||||
# onto attributes named `_designation`, `time`, `distance`, and `velocity`.
|
||||
# You should coerce these values to their appropriate data type and handle any edge cases.
|
||||
# The `cd_to_datetime` function will be useful.
|
||||
self._designation = ''
|
||||
self.time = None # TODO: Use the cd_to_datetime function for this attribute.
|
||||
self.distance = 0.0
|
||||
self.velocity = 0.0
|
||||
self._designation = info.get("designation")
|
||||
|
||||
self.time = info.get("time")
|
||||
if self.time:
|
||||
self.time = cd_to_datetime(info.get("time"))
|
||||
|
||||
self.distance = info.get("distance", float("nan"))
|
||||
self.velocity = info.get("velocity", float("nan"))
|
||||
|
||||
# Create an attribute for the referenced NEO, originally None.
|
||||
self.neo = None
|
||||
@@ -116,19 +132,23 @@ class CloseApproach:
|
||||
formatted string that can be used in human-readable representations and
|
||||
in serialization to CSV and JSON files.
|
||||
"""
|
||||
# TODO: Use this object's `.time` attribute and the `datetime_to_str` function to
|
||||
# build a formatted representation of the approach time.
|
||||
# TODO: Use self.designation and self.name to build a fullname for this object.
|
||||
return ''
|
||||
return f"{datetime_to_str(self.time)}"
|
||||
|
||||
def __str__(self):
|
||||
"""Return `str(self)`."""
|
||||
# TODO: Use this object's attributes to return a human-readable string representation.
|
||||
# The project instructions include one possibility. Peek at the __repr__
|
||||
# method for examples of advanced string formatting.
|
||||
return f"A CloseApproach ..."
|
||||
return f"On {self.time_str}, {self.neo.fullname} approaches Earth at a distance of {self.distance:.2f} au and a velocity of {self.velocity:.2f} km/s."
|
||||
|
||||
def __repr__(self):
|
||||
"""Return `repr(self)`, a computer-readable string representation of this object."""
|
||||
return f"CloseApproach(time={self.time_str!r}, distance={self.distance:.2f}, " \
|
||||
return (
|
||||
f"CloseApproach(time={self.time_str!r}, distance={self.distance:.2f}, "
|
||||
f"velocity={self.velocity:.2f}, neo={self.neo!r})"
|
||||
)
|
||||
|
||||
def serialize(self):
|
||||
"""Serialize this `CloseApproach` into a dictionary."""
|
||||
return {
|
||||
"datetime_utc": self.time_str,
|
||||
"distance_au": self.distance,
|
||||
"velocity_km_s": self.velocity,
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ extension determines which of these functions is used.
|
||||
|
||||
You'll edit this file in Part 4.
|
||||
"""
|
||||
|
||||
import csv
|
||||
import json
|
||||
|
||||
@@ -25,10 +26,24 @@ def write_to_csv(results, filename):
|
||||
:param filename: A Path-like object pointing to where the data should be saved.
|
||||
"""
|
||||
fieldnames = (
|
||||
'datetime_utc', 'distance_au', 'velocity_km_s',
|
||||
'designation', 'name', 'diameter_km', 'potentially_hazardous'
|
||||
"datetime_utc",
|
||||
"distance_au",
|
||||
"velocity_km_s",
|
||||
"designation",
|
||||
"name",
|
||||
"diameter_km",
|
||||
"potentially_hazardous",
|
||||
)
|
||||
# TODO: Write the results to a CSV file, following the specification in the instructions.
|
||||
with open(filename, "w") as csv_file:
|
||||
writer = csv.DictWriter(csv_file, fieldnames)
|
||||
writer.writeheader()
|
||||
for result in results:
|
||||
row = {**result.serialize(), **result.neo.serialize()}
|
||||
row["name"] = "" if row["name"] is None else row["name"]
|
||||
row["diameter_km"] = (
|
||||
"" if row["diameter_km"] is float("nan") else row["diameter_km"]
|
||||
)
|
||||
writer.writerow(row)
|
||||
|
||||
|
||||
def write_to_json(results, filename):
|
||||
@@ -42,4 +57,25 @@ def write_to_json(results, filename):
|
||||
:param results: An iterable of `CloseApproach` objects.
|
||||
:param filename: A Path-like object pointing to where the data should be saved.
|
||||
"""
|
||||
# TODO: Write the results to a JSON file, following the specification in the instructions.
|
||||
data = []
|
||||
for result in results:
|
||||
item = {**result.serialize(), **result.neo.serialize()}
|
||||
item["name"] = "" if item["name"] is None else item["name"]
|
||||
item["diameter_km"] = (
|
||||
"" if item["diameter_km"] is float("nan") else item["diameter_km"]
|
||||
)
|
||||
data.append(
|
||||
{
|
||||
"datetime_utc": item["datetime_utc"],
|
||||
"distance_au": item["distance_au"],
|
||||
"velocity_km_s": item["velocity_km_s"],
|
||||
"neo": {
|
||||
"designation": item["designation"],
|
||||
"name": item["name"],
|
||||
"diameter_km": item["diameter_km"],
|
||||
"potentially_hazardous": item["potentially_hazardous"],
|
||||
},
|
||||
}
|
||||
)
|
||||
with open(filename, "w") as json_file:
|
||||
json.dump(data, json_file, indent=2)
|
||||
|
||||
35
Meme_Generator/MemeEngine/MemeEngine.py
Normal file
35
Meme_Generator/MemeEngine/MemeEngine.py
Normal file
@@ -0,0 +1,35 @@
|
||||
"""Meme engine module for handling the generation of meme images."""
|
||||
|
||||
import random
|
||||
from PIL import Image, ImageFont, ImageDraw
|
||||
|
||||
|
||||
class MemeEngine:
|
||||
"""Class to generate memes from images and text."""
|
||||
|
||||
def __init__(self, output_path):
|
||||
"""Initialize meme engine with path to save generated memes."""
|
||||
self.output_path = output_path
|
||||
|
||||
def make_meme(self, img_path, text, author, width=500) -> str:
|
||||
"""Generate a meme image with given text and author."""
|
||||
output_file = f"{self.output_path}/{random.randint(0,10000)}.jpg"
|
||||
message = f"{text}\n- {author}"
|
||||
|
||||
try:
|
||||
with Image.open(img_path) as img:
|
||||
# Resize image while maintaining aspect ratio
|
||||
width = 500 if img.size[0] > 500 else img.size[0]
|
||||
ratio = width / (img.size[0] * 1.0)
|
||||
height = ratio * img.size[1]
|
||||
img = img.resize((int(width), int(height)), Image.NEAREST)
|
||||
draw = ImageDraw.Draw(img)
|
||||
font = ImageFont.truetype(
|
||||
"./_data/font/calibri_regular.ttf", int(height / 20)
|
||||
)
|
||||
draw.text((20, 20), message, font=font, fill="white")
|
||||
img.save(output_file)
|
||||
except Exception as err:
|
||||
print(f"Error: {err}")
|
||||
|
||||
return output_file
|
||||
1
Meme_Generator/MemeEngine/__init__.py
Normal file
1
Meme_Generator/MemeEngine/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from .MemeEngine import MemeEngine
|
||||
24
Meme_Generator/QuoteEngine/CSVIngestor.py
Normal file
24
Meme_Generator/QuoteEngine/CSVIngestor.py
Normal file
@@ -0,0 +1,24 @@
|
||||
"""Module for ingesting CSV files containing quotes."""
|
||||
|
||||
from typing import List
|
||||
from .IngestorInterface import IngestorInterface
|
||||
from .QuoteModel import QuoteModel
|
||||
|
||||
import pandas as pd
|
||||
|
||||
|
||||
class CSVIngestor(IngestorInterface):
|
||||
"""Subclass for ingesting CSV files."""
|
||||
|
||||
allowed_extensions = ["csv"]
|
||||
|
||||
@classmethod
|
||||
def parse(cls, path: str) -> List[QuoteModel]:
|
||||
"""Parse the CSV file to extract quotes."""
|
||||
if not cls.can_ingest(path):
|
||||
raise Exception("Invalid ingest path")
|
||||
quotes = []
|
||||
df = pd.read_csv(path, header=0, sep=",", names=["body", "author"])
|
||||
for _, row in df.iterrows():
|
||||
quotes.append(QuoteModel(row["body"], row["author"]))
|
||||
return quotes
|
||||
25
Meme_Generator/QuoteEngine/DocxIngestor.py
Normal file
25
Meme_Generator/QuoteEngine/DocxIngestor.py
Normal file
@@ -0,0 +1,25 @@
|
||||
"""Module for ingesting Docx files containing quotes."""
|
||||
|
||||
import docx
|
||||
from typing import List
|
||||
from .IngestorInterface import IngestorInterface
|
||||
from .QuoteModel import QuoteModel
|
||||
|
||||
|
||||
class DocxIngestor(IngestorInterface):
|
||||
"""Subclass for ingesting Docx files."""
|
||||
|
||||
allowed_extensions = ["docx"]
|
||||
|
||||
@classmethod
|
||||
def parse(cls, path: str) -> List[QuoteModel]:
|
||||
"""Parse the Docx file to extract quotes."""
|
||||
if not cls.can_ingest(path):
|
||||
raise Exception("Invalid ingest path")
|
||||
quotes = []
|
||||
doc = docx.Document(path)
|
||||
for para in doc.paragraphs:
|
||||
if para.text != "":
|
||||
parts = para.text.split(" - ")
|
||||
quotes.append(QuoteModel(parts[0], parts[1]))
|
||||
return quotes
|
||||
22
Meme_Generator/QuoteEngine/Ingestor.py
Normal file
22
Meme_Generator/QuoteEngine/Ingestor.py
Normal file
@@ -0,0 +1,22 @@
|
||||
"""Ingestor module to select appropriate ingestor based on file type."""
|
||||
|
||||
from typing import List
|
||||
from .IngestorInterface import IngestorInterface
|
||||
from .QuoteModel import QuoteModel
|
||||
from .CSVIngestor import CSVIngestor
|
||||
from .TextIngestor import TextIngestor
|
||||
from .DocxIngestor import DocxIngestor
|
||||
from .PDFIngestor import PDFIngestor
|
||||
|
||||
|
||||
class Ingestor(IngestorInterface):
|
||||
"""Subclass to select appropriate ingestor."""
|
||||
|
||||
ingestors = [CSVIngestor, TextIngestor, DocxIngestor, PDFIngestor]
|
||||
|
||||
@classmethod
|
||||
def parse(cls, path: str) -> List[QuoteModel]:
|
||||
"""Select the appropriate ingestor to parse the file."""
|
||||
for ingestor in cls.ingestors:
|
||||
if ingestor.can_ingest(path):
|
||||
return ingestor.parse(path)
|
||||
23
Meme_Generator/QuoteEngine/IngestorInterface.py
Normal file
23
Meme_Generator/QuoteEngine/IngestorInterface.py
Normal file
@@ -0,0 +1,23 @@
|
||||
"""Ingestor Interface module for quote ingestion."""
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import List
|
||||
from .QuoteModel import QuoteModel
|
||||
|
||||
|
||||
class IngestorInterface(ABC):
|
||||
"""Base Ingestor Interface."""
|
||||
|
||||
allowed_extensions = []
|
||||
|
||||
@classmethod
|
||||
def can_ingest(cls, path: str) -> bool:
|
||||
"""Check if the ingestor can ingest the file based on its extension."""
|
||||
ext = path.split(".")[-1]
|
||||
return ext in cls.allowed_extensions
|
||||
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
def parse(cls, path: str) -> List[QuoteModel]:
|
||||
"""Abstract method to parse the file and return a list of QuoteModel objects."""
|
||||
pass
|
||||
40
Meme_Generator/QuoteEngine/PDFIngestor.py
Normal file
40
Meme_Generator/QuoteEngine/PDFIngestor.py
Normal file
@@ -0,0 +1,40 @@
|
||||
"""Module for ingesting PDF files containing quotes."""
|
||||
|
||||
import os
|
||||
import random
|
||||
import subprocess
|
||||
from typing import List
|
||||
from .IngestorInterface import IngestorInterface
|
||||
from .QuoteModel import QuoteModel
|
||||
|
||||
|
||||
class PDFIngestor(IngestorInterface):
|
||||
"""Subclass for ingesting PDF files."""
|
||||
|
||||
allowed_extensions = ["pdf"]
|
||||
|
||||
@classmethod
|
||||
def parse(cls, path: str) -> List[QuoteModel]:
|
||||
"""Parse the PDF file to extract quotes."""
|
||||
if not cls.can_ingest(path):
|
||||
raise Exception("Invalid ingest path")
|
||||
|
||||
quotes = []
|
||||
tmp = f"./tmp/{random.randint(0, 10000)}.txt"
|
||||
try:
|
||||
# pdftotext <input-pdf> <output-text-file>
|
||||
call = subprocess.call(["pdftotext", path, tmp])
|
||||
with open(tmp, "r") as file:
|
||||
lines = file.readlines()
|
||||
except FileNotFoundError as err:
|
||||
print(f"Error: {err}")
|
||||
else:
|
||||
for line in lines:
|
||||
line = line.strip()
|
||||
if line:
|
||||
parts = line.split(" - ")
|
||||
quotes.append(QuoteModel(parts[0], parts[1]))
|
||||
finally:
|
||||
if os.path.exists(tmp):
|
||||
os.remove(tmp)
|
||||
return quotes
|
||||
14
Meme_Generator/QuoteEngine/QuoteModel.py
Normal file
14
Meme_Generator/QuoteEngine/QuoteModel.py
Normal file
@@ -0,0 +1,14 @@
|
||||
"""QuoteModel module for representing a quote with its body and author."""
|
||||
|
||||
|
||||
class QuoteModel:
|
||||
"""Quote model class."""
|
||||
|
||||
def __init__(self, body, author):
|
||||
"""Initialize the QuoteModel object."""
|
||||
self.body = body
|
||||
self.author = author
|
||||
|
||||
def __repr__(self):
|
||||
"""String representation of the QuoteModel object."""
|
||||
return f"{self.body} - {self.author}"
|
||||
26
Meme_Generator/QuoteEngine/TextIngestor.py
Normal file
26
Meme_Generator/QuoteEngine/TextIngestor.py
Normal file
@@ -0,0 +1,26 @@
|
||||
"""Module for ingesting text files containing quotes."""
|
||||
|
||||
from typing import List
|
||||
from .IngestorInterface import IngestorInterface
|
||||
from .QuoteModel import QuoteModel
|
||||
|
||||
|
||||
class TextIngestor(IngestorInterface):
|
||||
"""Subcalss for ingesting text files."""
|
||||
|
||||
allowed_extensions = ["txt"]
|
||||
|
||||
@classmethod
|
||||
def parse(cls, path: str) -> List[QuoteModel]:
|
||||
"""Parse the text file to extract quotes."""
|
||||
if not cls.can_ingest(path):
|
||||
raise Exception("Invalid ingest path")
|
||||
quotes = []
|
||||
with open(path, "r") as file:
|
||||
for line in file.readlines():
|
||||
line = line.strip()
|
||||
if line:
|
||||
parts = line.split(" - ", 1)
|
||||
if len(parts) == 2:
|
||||
quotes.append(QuoteModel(parts[0], parts[1]))
|
||||
return quotes
|
||||
7
Meme_Generator/QuoteEngine/__init__.py
Normal file
7
Meme_Generator/QuoteEngine/__init__.py
Normal file
@@ -0,0 +1,7 @@
|
||||
from .IngestorInterface import IngestorInterface
|
||||
from .CSVIngestor import CSVIngestor
|
||||
from .DocxIngestor import DocxIngestor
|
||||
from .PDFIngestor import PDFIngestor
|
||||
from .TextIngestor import TextIngestor
|
||||
from .Ingestor import Ingestor
|
||||
from .QuoteModel import QuoteModel
|
||||
39
Meme_Generator/README.md
Normal file
39
Meme_Generator/README.md
Normal file
@@ -0,0 +1,39 @@
|
||||
## Meme Generator Project
|
||||
|
||||
### Overview
|
||||
The goal of this project is to build a "meme generator" – a multimedia application to dynamically generate memes, including an image with an overlaid quote.
|
||||
|
||||
|
||||
### Set up environment
|
||||
```sh
|
||||
$ sudo apt-get install -y xpdf
|
||||
$ pip install -r requirements.txt
|
||||
```
|
||||
|
||||
### Run program
|
||||
|
||||
Python test \
|
||||
Usage: `python3 meme.py --path [image_path] --body [quote_body] --author [quote_author]`
|
||||
```sh
|
||||
$ mkdir tmp
|
||||
$ python3 meme.py
|
||||
```
|
||||
|
||||
Flask test
|
||||
```sh
|
||||
$ mkdir static
|
||||
$ python3 app.py
|
||||
```
|
||||
Open link `http://127.0.0.1:5000` in web browser
|
||||
|
||||
### Primary modules
|
||||
|
||||
`QuoteEngine`: Meme engine module for handling the generation of meme images.\
|
||||
`MemeEngine`: QuoteModel module for representing a quote with its body and author.
|
||||
|
||||
### Sub modules
|
||||
|
||||
`CSVIngestor`: Module for ingesting CSV files containing quotes. \
|
||||
`DocxIngestor`: Module for ingesting Docx files containing quotes. \
|
||||
`TextIngestor`: Module for ingesting text files containing quotes. \
|
||||
`PDFIngestor`: Module for ingesting PDF files containing quotes.
|
||||
3
Meme_Generator/_data/DogQuotes/DogQuotesCSV.csv
Normal file
3
Meme_Generator/_data/DogQuotes/DogQuotesCSV.csv
Normal file
@@ -0,0 +1,3 @@
|
||||
body,author
|
||||
Chase the mailman,Skittle
|
||||
"When in doubt, go shoe-shopping",Mr. Paws
|
||||
|
BIN
Meme_Generator/_data/DogQuotes/DogQuotesDOCX.docx
Normal file
BIN
Meme_Generator/_data/DogQuotes/DogQuotesDOCX.docx
Normal file
Binary file not shown.
BIN
Meme_Generator/_data/DogQuotes/DogQuotesPDF.pdf
Normal file
BIN
Meme_Generator/_data/DogQuotes/DogQuotesPDF.pdf
Normal file
Binary file not shown.
2
Meme_Generator/_data/DogQuotes/DogQuotesTXT.txt
Normal file
2
Meme_Generator/_data/DogQuotes/DogQuotesTXT.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
To bork or not to bork - Bork
|
||||
He who smelt it... - Stinky
|
||||
6
Meme_Generator/_data/SimpleLines/SimpleLines.csv
Normal file
6
Meme_Generator/_data/SimpleLines/SimpleLines.csv
Normal file
@@ -0,0 +1,6 @@
|
||||
body,author
|
||||
Line 1,Author 1
|
||||
Line 2,Author 2
|
||||
Line 3,Author 3
|
||||
Line 4,Author 4
|
||||
Line 5,Author 5
|
||||
|
BIN
Meme_Generator/_data/SimpleLines/SimpleLines.docx
Normal file
BIN
Meme_Generator/_data/SimpleLines/SimpleLines.docx
Normal file
Binary file not shown.
BIN
Meme_Generator/_data/SimpleLines/SimpleLines.pdf
Normal file
BIN
Meme_Generator/_data/SimpleLines/SimpleLines.pdf
Normal file
Binary file not shown.
7
Meme_Generator/_data/SimpleLines/SimpleLines.txt
Normal file
7
Meme_Generator/_data/SimpleLines/SimpleLines.txt
Normal file
@@ -0,0 +1,7 @@
|
||||
"Line 1" - Author 1
|
||||
"Line 2" - Author 2
|
||||
"Line 3" - Author 3
|
||||
"Line 4" - Author 4
|
||||
"Line 5" - Author 5
|
||||
|
||||
|
||||
BIN
Meme_Generator/_data/font/calibri_regular.ttf
Normal file
BIN
Meme_Generator/_data/font/calibri_regular.ttf
Normal file
Binary file not shown.
BIN
Meme_Generator/_data/photos/dog/xander_1.jpg
Normal file
BIN
Meme_Generator/_data/photos/dog/xander_1.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.7 MiB |
BIN
Meme_Generator/_data/photos/dog/xander_2.jpg
Normal file
BIN
Meme_Generator/_data/photos/dog/xander_2.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 95 KiB |
BIN
Meme_Generator/_data/photos/dog/xander_3.jpg
Normal file
BIN
Meme_Generator/_data/photos/dog/xander_3.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 28 KiB |
BIN
Meme_Generator/_data/photos/dog/xander_4.jpg
Normal file
BIN
Meme_Generator/_data/photos/dog/xander_4.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 35 KiB |
112
Meme_Generator/app.py
Normal file
112
Meme_Generator/app.py
Normal file
@@ -0,0 +1,112 @@
|
||||
import random
|
||||
import os
|
||||
import requests
|
||||
from flask import Flask, render_template, abort, request
|
||||
|
||||
from QuoteEngine import Ingestor
|
||||
from MemeEngine import MemeEngine
|
||||
|
||||
# Create the Flask application object. "__name__" tells Flask where to find templates and static files.
|
||||
app = Flask(__name__)
|
||||
|
||||
# Create a global MemeEngine instance that will write generated memes into "./static"
|
||||
# so that the images can be served by the web server.
|
||||
meme = MemeEngine("./static")
|
||||
|
||||
|
||||
def setup():
|
||||
"""Load all resources"""
|
||||
|
||||
quote_files = [
|
||||
"./_data/DogQuotes/DogQuotesTXT.txt",
|
||||
"./_data/DogQuotes/DogQuotesDOCX.docx",
|
||||
"./_data/DogQuotes/DogQuotesPDF.pdf",
|
||||
"./_data/DogQuotes/DogQuotesCSV.csv",
|
||||
]
|
||||
|
||||
quotes = []
|
||||
for file in quote_files:
|
||||
if Ingestor.parse(file) is not None:
|
||||
quotes.append(Ingestor.parse(file))
|
||||
|
||||
images_path = "./_data/photos/dog/"
|
||||
|
||||
imgs = []
|
||||
for root, _, files in os.walk(images_path):
|
||||
imgs = [os.path.join(root, name) for name in files]
|
||||
|
||||
return quotes, imgs
|
||||
|
||||
|
||||
# Call setup() once at import time to preload quotes and images.
|
||||
# These will be reused on every request.
|
||||
quotes, imgs = setup()
|
||||
|
||||
|
||||
@app.route("/")
|
||||
def meme_rand():
|
||||
"""Generate a random meme
|
||||
|
||||
Steps:
|
||||
- Pick a random image from imgs
|
||||
- Pick a random "quote list" from quotes
|
||||
- Pick a random quote from that list
|
||||
- Generate a meme image with that quote and image
|
||||
- Render a template to show the meme
|
||||
"""
|
||||
img = random.choice(imgs)
|
||||
quote_list = random.choice(quotes)
|
||||
quote = random.choice(quote_list)
|
||||
path = meme.make_meme(img, quote.body, quote.author)
|
||||
return render_template("meme.html", path=path)
|
||||
|
||||
|
||||
@app.route("/create", methods=["GET"])
|
||||
def meme_form():
|
||||
"""User input for meme information
|
||||
|
||||
This route renders a form where the user can input:
|
||||
- image_url: URL of the source image
|
||||
- body: quote text
|
||||
- author: quote author
|
||||
"""
|
||||
return render_template("meme_form.html")
|
||||
|
||||
|
||||
@app.route("/create", methods=["POST"])
|
||||
def meme_post():
|
||||
"""Create a user defined meme
|
||||
|
||||
This route:
|
||||
- Reads form data sent via POST from the meme_form page
|
||||
- Downloads the image from the provided URL
|
||||
- Saves it to a temporary file
|
||||
- Passes that file to MemeEngine to generate a meme
|
||||
- Deletes the temporary file
|
||||
- Renders the final meme
|
||||
"""
|
||||
|
||||
image_url = request.form.get("image_url", "").strip()
|
||||
body = request.form.get("body", "").strip()
|
||||
author = request.form.get("author", "").strip()
|
||||
image = requests.get(image_url, timeout=5)
|
||||
|
||||
try:
|
||||
tmp_file = f"tmp/{random.randint(0, 10000)}.jpg"
|
||||
with open(tmp_file, "wb") as file:
|
||||
file.write(image.content)
|
||||
except:
|
||||
print("Failed to generate meme")
|
||||
path = None
|
||||
if os.path.exists(tmp_file):
|
||||
os.remove(tmp_file)
|
||||
else:
|
||||
path = meme.make_meme(tmp_file, body, author)
|
||||
if os.path.exists(tmp_file):
|
||||
os.remove(tmp_file)
|
||||
finally:
|
||||
return render_template("meme.html", path=path)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run()
|
||||
54
Meme_Generator/meme.py
Normal file
54
Meme_Generator/meme.py
Normal file
@@ -0,0 +1,54 @@
|
||||
import os
|
||||
import random
|
||||
import argparse
|
||||
from MemeEngine import MemeEngine
|
||||
from QuoteEngine import QuoteModel, Ingestor
|
||||
|
||||
|
||||
def generate_meme(path=None, body=None, author=None):
|
||||
"""Generate a meme given an path and a quote"""
|
||||
img = None
|
||||
quote = None
|
||||
|
||||
if path is None:
|
||||
images = "./_data/photos/dog/"
|
||||
imgs = []
|
||||
for root, dirs, files in os.walk(images):
|
||||
imgs = [os.path.join(root, name) for name in files]
|
||||
|
||||
img = random.choice(imgs)
|
||||
else:
|
||||
img = path[0]
|
||||
|
||||
if body is None:
|
||||
quote_files = [
|
||||
"./_data/DogQuotes/DogQuotesTXT.txt",
|
||||
"./_data/DogQuotes/DogQuotesDOCX.docx",
|
||||
"./_data/DogQuotes/DogQuotesPDF.pdf",
|
||||
"./_data/DogQuotes/DogQuotesCSV.csv",
|
||||
]
|
||||
quotes = []
|
||||
for f in quote_files:
|
||||
quotes.extend(Ingestor.parse(f))
|
||||
|
||||
quote = random.choice(quotes)
|
||||
else:
|
||||
if author is None:
|
||||
raise Exception("Author Required if Body is Used")
|
||||
quote = QuoteModel(body, author)
|
||||
|
||||
meme = MemeEngine("./tmp")
|
||||
|
||||
path = meme.make_meme(img, quote.body, quote.author)
|
||||
|
||||
return path
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
"""Add path, quote, and author arguments for CLI, then print meme generation."""
|
||||
parser = argparse.ArgumentParser(description="Meme Generator")
|
||||
parser.add_argument("--path", type=str, help="Image path")
|
||||
parser.add_argument("--body", type=str, help="Quote adding to meme")
|
||||
parser.add_argument("--author", type=str, help="Author adding to meme")
|
||||
args = parser.parse_args()
|
||||
print(generate_meme(args.path, args.body, args.author))
|
||||
29
Meme_Generator/requirements.txt
Normal file
29
Meme_Generator/requirements.txt
Normal file
@@ -0,0 +1,29 @@
|
||||
black==25.12.0
|
||||
blinker==1.9.0
|
||||
certifi==2025.11.12
|
||||
charset-normalizer==3.4.4
|
||||
click==8.3.1
|
||||
Flask==3.1.2
|
||||
idna==3.11
|
||||
itsdangerous==2.2.0
|
||||
Jinja2==3.1.6
|
||||
lxml==6.0.2
|
||||
MarkupSafe==3.0.3
|
||||
mypy_extensions==1.1.0
|
||||
numpy==2.2.6
|
||||
packaging==25.0
|
||||
pandas==2.3.3
|
||||
pathspec==0.12.1
|
||||
pillow==12.1.0
|
||||
platformdirs==4.5.1
|
||||
python-dateutil==2.9.0.post0
|
||||
python-docx==1.2.0
|
||||
pytokens==0.3.0
|
||||
pytz==2025.2
|
||||
requests==2.32.5
|
||||
six==1.17.0
|
||||
tomli==2.3.0
|
||||
typing_extensions==4.15.0
|
||||
tzdata==2025.3
|
||||
urllib3==2.6.2
|
||||
Werkzeug==3.1.4
|
||||
37
Meme_Generator/templates/base.html
Normal file
37
Meme_Generator/templates/base.html
Normal file
@@ -0,0 +1,37 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<title>{% block title %}{% endblock %}</title>
|
||||
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
|
||||
<style>
|
||||
body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
background: #F5FCFE;
|
||||
}
|
||||
|
||||
.nav {
|
||||
display: inline;
|
||||
padding: 60px;
|
||||
}
|
||||
|
||||
img {
|
||||
border: 1px solid rgba(0,0,0,.125);
|
||||
border-radius: .25rem;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
{% block body %}{% endblock %}
|
||||
|
||||
<div class="nav">
|
||||
<a class="btn btn-primary" href="{{url_for('meme_rand')}}">Random</a>
|
||||
<a class="btn btn-outline-primary" href="{{url_for('meme_form')}}">Creator</a>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
5
Meme_Generator/templates/meme.html
Normal file
5
Meme_Generator/templates/meme.html
Normal file
@@ -0,0 +1,5 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Meme Generator{% endblock %}
|
||||
{% block body %}
|
||||
<img src="{{ path }}" />
|
||||
{% endblock %}
|
||||
23
Meme_Generator/templates/meme_form.html
Normal file
23
Meme_Generator/templates/meme_form.html
Normal file
@@ -0,0 +1,23 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Meme Generator{% endblock %}
|
||||
{% block body %}
|
||||
<div class="card" style="width: 500px; max-width: 100%;">
|
||||
<div class="card-body">
|
||||
<form action="{{url_for('meme_post')}}" method="POST">
|
||||
<div class="form-group">
|
||||
<label for="image_url">Image URL</label>
|
||||
<input type="url" class="form-control" id="image_url" aria-describedby="image url" placeholder="Enter a url for an image" name="image_url">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="body">Quote Body</label>
|
||||
<input type="text" class="form-control" id="body" aria-describedby="Quote Body" placeholder="To be or not to be" name="body">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="author">Quote Author</label>
|
||||
<input type="text" class="form-control" id="author" aria-describedby="Quote Author" placeholder="Shakespeare" name="author">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Create Meme!</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user