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
|
# PyPI configuration file
|
||||||
.pypirc
|
.pypirc
|
||||||
|
|
||||||
|
|
||||||
|
# VS Code
|
||||||
|
.vscode/
|
||||||
@@ -21,6 +21,7 @@ class NEODatabase:
|
|||||||
help fetch NEOs by primary designation or by name and to help speed up
|
help fetch NEOs by primary designation or by name and to help speed up
|
||||||
querying for close approaches that match criteria.
|
querying for close approaches that match criteria.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, neos, approaches):
|
def __init__(self, neos, approaches):
|
||||||
"""Create a new `NEODatabase`.
|
"""Create a new `NEODatabase`.
|
||||||
|
|
||||||
@@ -42,9 +43,16 @@ class NEODatabase:
|
|||||||
self._neos = neos
|
self._neos = neos
|
||||||
self._approaches = approaches
|
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):
|
def get_neo_by_designation(self, designation):
|
||||||
"""Find and return an NEO by its primary 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.
|
:param designation: The primary designation of the NEO to search for.
|
||||||
:return: The `NearEarthObject` with the desired primary designation, or `None`.
|
:return: The `NearEarthObject` with the desired primary designation, or `None`.
|
||||||
"""
|
"""
|
||||||
# TODO: Fetch an NEO by its primary designation.
|
return self._neo_by_designation.get(designation, None)
|
||||||
return None
|
|
||||||
|
|
||||||
def get_neo_by_name(self, name):
|
def get_neo_by_name(self, name):
|
||||||
"""Find and return an NEO by its 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.
|
:param name: The name, as a string, of the NEO to search for.
|
||||||
:return: The `NearEarthObject` with the desired name, or `None`.
|
:return: The `NearEarthObject` with the desired name, or `None`.
|
||||||
"""
|
"""
|
||||||
# TODO: Fetch an NEO by its name.
|
return self._neo_by_name.get(name, None)
|
||||||
return None
|
|
||||||
|
|
||||||
def query(self, filters=()):
|
def query(self, filters=()):
|
||||||
"""Query close approaches to generate those that match a collection of 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.
|
:param filters: A collection of filters capturing user-specified criteria.
|
||||||
:return: A stream of matching `CloseApproach` objects.
|
:return: A stream of matching `CloseApproach` objects.
|
||||||
"""
|
"""
|
||||||
# TODO: Generate `CloseApproach` objects that match all of the filters.
|
|
||||||
for approach in self._approaches:
|
for approach in self._approaches:
|
||||||
|
cases = [filter_i(approach) for filter_i in filters]
|
||||||
|
if all(cases):
|
||||||
yield approach
|
yield approach
|
||||||
|
|||||||
@@ -12,9 +12,9 @@ line, and uses the resulting collections to build an `NEODatabase`.
|
|||||||
|
|
||||||
You'll edit this file in Task 2.
|
You'll edit this file in Task 2.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import csv
|
import csv
|
||||||
import json
|
import json
|
||||||
|
|
||||||
from models import NearEarthObject, CloseApproach
|
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.
|
:param neo_csv_path: A path to a CSV file containing data about near-Earth objects.
|
||||||
:return: A collection of `NearEarthObject`s.
|
:return: A collection of `NearEarthObject`s.
|
||||||
"""
|
"""
|
||||||
# TODO: Load NEO data from the given CSV file.
|
neos = []
|
||||||
return ()
|
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):
|
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.
|
:param cad_json_path: A path to a JSON file containing data about close approaches.
|
||||||
:return: A collection of `CloseApproach`es.
|
:return: A collection of `CloseApproach`es.
|
||||||
"""
|
"""
|
||||||
# TODO: Load close approach data from the given JSON file.
|
approaches = []
|
||||||
return ()
|
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.
|
You'll edit this file in Tasks 3a and 3c.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import operator
|
import operator
|
||||||
|
import itertools
|
||||||
|
|
||||||
|
|
||||||
class UnsupportedCriterionError(NotImplementedError):
|
class UnsupportedCriterionError(NotImplementedError):
|
||||||
@@ -38,6 +40,7 @@ class AttributeFilter:
|
|||||||
Concrete subclasses can override the `get` classmethod to provide custom
|
Concrete subclasses can override the `get` classmethod to provide custom
|
||||||
behavior to fetch a desired attribute from the given `CloseApproach`.
|
behavior to fetch a desired attribute from the given `CloseApproach`.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, op, value):
|
def __init__(self, op, value):
|
||||||
"""Construct a new `AttributeFilter` from an binary predicate and a reference 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})"
|
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(
|
def create_filters(
|
||||||
date=None, start_date=None, end_date=None,
|
date=None,
|
||||||
distance_min=None, distance_max=None,
|
start_date=None,
|
||||||
velocity_min=None, velocity_max=None,
|
end_date=None,
|
||||||
diameter_min=None, diameter_max=None,
|
distance_min=None,
|
||||||
hazardous=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.
|
"""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.
|
:param hazardous: Whether the NEO of a matching `CloseApproach` is potentially hazardous.
|
||||||
:return: A collection of filters for use with `query`.
|
:return: A collection of filters for use with `query`.
|
||||||
"""
|
"""
|
||||||
# TODO: Decide how you will represent your filters.
|
filters = []
|
||||||
return ()
|
|
||||||
|
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):
|
def limit(iterator, n=None):
|
||||||
@@ -121,5 +191,4 @@ def limit(iterator, n=None):
|
|||||||
:param n: The maximum number of values to produce.
|
:param n: The maximum number of values to produce.
|
||||||
:yield: The first (at most) `n` values from the iterator.
|
:yield: The first (at most) `n` values from the iterator.
|
||||||
"""
|
"""
|
||||||
# TODO: Produce at most `n` values from the given iterator.
|
return itertools.islice(iterator, n) if n else iterator
|
||||||
return 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.
|
You'll edit this file in Task 1.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from helpers import cd_to_datetime, datetime_to_str
|
from helpers import cd_to_datetime, datetime_to_str
|
||||||
|
import math
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
|
||||||
class NearEarthObject:
|
class NearEarthObject:
|
||||||
@@ -32,22 +35,20 @@ class NearEarthObject:
|
|||||||
initialized to an empty collection, but eventually populated in the
|
initialized to an empty collection, but eventually populated in the
|
||||||
`NEODatabase` constructor.
|
`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):
|
def __init__(self, **info):
|
||||||
"""Create a new `NearEarthObject`.
|
"""Create a new `NearEarthObject`.
|
||||||
|
|
||||||
:param info: A dictionary of excess keyword arguments supplied to the constructor.
|
:param info: A dictionary of excess keyword arguments supplied to the constructor.
|
||||||
"""
|
"""
|
||||||
# TODO: Assign information from the arguments passed to the constructor
|
self.designation = info.get("designation")
|
||||||
# onto attributes named `designation`, `name`, `diameter`, and `hazardous`.
|
self.name = None if info.get("name") == "" else info.get("name")
|
||||||
# You should coerce these values to their appropriate data type and
|
self.diameter = (
|
||||||
# handle any edge cases, such as a empty name being represented by `None`
|
float("nan")
|
||||||
# and a missing diameter being represented by `float('nan')`.
|
if info.get("diameter") in (None, "")
|
||||||
self.designation = ''
|
else float(info.get("diameter"))
|
||||||
self.name = None
|
)
|
||||||
self.diameter = float('nan')
|
self.hazardous = info.get("hazardous", "N") == "Y"
|
||||||
self.hazardous = False
|
|
||||||
|
|
||||||
# Create an empty initial collection of linked approaches.
|
# Create an empty initial collection of linked approaches.
|
||||||
self.approaches = []
|
self.approaches = []
|
||||||
@@ -55,20 +56,36 @@ class NearEarthObject:
|
|||||||
@property
|
@property
|
||||||
def fullname(self):
|
def fullname(self):
|
||||||
"""Return a representation of the full name of this NEO."""
|
"""Return a representation of the full name of this NEO."""
|
||||||
# TODO: Use self.designation and self.name to build a fullname for this object.
|
if self.name:
|
||||||
return ''
|
return f"{self.designation} {self.name}"
|
||||||
|
return f"{self.designation}"
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
"""Return `str(self)`."""
|
"""Return `str(self)`."""
|
||||||
# TODO: Use this object's attributes to return a human-readable string representation.
|
hazardous_str = (
|
||||||
# The project instructions include one possibility. Peek at the __repr__
|
"is potentially hazardous"
|
||||||
# method for examples of advanced string formatting.
|
if self.hazardous
|
||||||
return f"A NearEarthObject ..."
|
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):
|
def __repr__(self):
|
||||||
"""Return `repr(self)`, a computer-readable string representation of this object."""
|
"""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})"
|
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:
|
class CloseApproach:
|
||||||
@@ -84,21 +101,20 @@ class CloseApproach:
|
|||||||
private attribute, but the referenced NEO is eventually replaced in the
|
private attribute, but the referenced NEO is eventually replaced in the
|
||||||
`NEODatabase` constructor.
|
`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):
|
def __init__(self, **info):
|
||||||
"""Create a new `CloseApproach`.
|
"""Create a new `CloseApproach`.
|
||||||
|
|
||||||
:param info: A dictionary of excess keyword arguments supplied to the constructor.
|
:param info: A dictionary of excess keyword arguments supplied to the constructor.
|
||||||
"""
|
"""
|
||||||
# TODO: Assign information from the arguments passed to the constructor
|
self._designation = info.get("designation")
|
||||||
# onto attributes named `_designation`, `time`, `distance`, and `velocity`.
|
|
||||||
# You should coerce these values to their appropriate data type and handle any edge cases.
|
self.time = info.get("time")
|
||||||
# The `cd_to_datetime` function will be useful.
|
if self.time:
|
||||||
self._designation = ''
|
self.time = cd_to_datetime(info.get("time"))
|
||||||
self.time = None # TODO: Use the cd_to_datetime function for this attribute.
|
|
||||||
self.distance = 0.0
|
self.distance = info.get("distance", float("nan"))
|
||||||
self.velocity = 0.0
|
self.velocity = info.get("velocity", float("nan"))
|
||||||
|
|
||||||
# Create an attribute for the referenced NEO, originally None.
|
# Create an attribute for the referenced NEO, originally None.
|
||||||
self.neo = None
|
self.neo = None
|
||||||
@@ -116,19 +132,23 @@ class CloseApproach:
|
|||||||
formatted string that can be used in human-readable representations and
|
formatted string that can be used in human-readable representations and
|
||||||
in serialization to CSV and JSON files.
|
in serialization to CSV and JSON files.
|
||||||
"""
|
"""
|
||||||
# TODO: Use this object's `.time` attribute and the `datetime_to_str` function to
|
return f"{datetime_to_str(self.time)}"
|
||||||
# build a formatted representation of the approach time.
|
|
||||||
# TODO: Use self.designation and self.name to build a fullname for this object.
|
|
||||||
return ''
|
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
"""Return `str(self)`."""
|
"""Return `str(self)`."""
|
||||||
# TODO: Use this object's attributes to return a human-readable string representation.
|
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."
|
||||||
# The project instructions include one possibility. Peek at the __repr__
|
|
||||||
# method for examples of advanced string formatting.
|
|
||||||
return f"A CloseApproach ..."
|
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
"""Return `repr(self)`, a computer-readable string representation of this object."""
|
"""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})"
|
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.
|
You'll edit this file in Part 4.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import csv
|
import csv
|
||||||
import json
|
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.
|
:param filename: A Path-like object pointing to where the data should be saved.
|
||||||
"""
|
"""
|
||||||
fieldnames = (
|
fieldnames = (
|
||||||
'datetime_utc', 'distance_au', 'velocity_km_s',
|
"datetime_utc",
|
||||||
'designation', 'name', 'diameter_km', 'potentially_hazardous'
|
"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):
|
def write_to_json(results, filename):
|
||||||
@@ -42,4 +57,25 @@ def write_to_json(results, filename):
|
|||||||
:param results: An iterable of `CloseApproach` objects.
|
:param results: An iterable of `CloseApproach` objects.
|
||||||
:param filename: A Path-like object pointing to where the data should be saved.
|
: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