From 28808c5e0e5178c1194c8be48f26de8ca464d131 Mon Sep 17 00:00:00 2001 From: joeriddles Date: Thu, 12 Sep 2024 23:06:25 -0700 Subject: [PATCH 1/2] Scrape and save event images from Meetup --- src/web/scrapers.py | 69 +++++++++++++++++++-------- src/web/services.py | 20 +++++++- src/web/tests/data/meetup-image.jpeg | Bin 0 -> 51328 bytes src/web/tests/data/meetup-image.webp | Bin 0 -> 11840 bytes src/web/tests/test_scrapers.py | 46 +++++++++++++++--- 5 files changed, 106 insertions(+), 29 deletions(-) create mode 100644 src/web/tests/data/meetup-image.jpeg create mode 100644 src/web/tests/data/meetup-image.webp diff --git a/src/web/scrapers.py b/src/web/scrapers.py index 8476185d..e0d41dd9 100644 --- a/src/web/scrapers.py +++ b/src/web/scrapers.py @@ -3,13 +3,14 @@ import pathlib import re import urllib.parse +import zoneinfo from datetime import datetime, timedelta from typing import Any, Protocol, TypeAlias, TypeVar import eventbrite.access_methods import requests -import zoneinfo -from bs4 import BeautifulSoup, Tag +from bs4 import BeautifulSoup +from bs4.element import Tag from django.conf import settings from django.utils import timezone from eventbrite import Eventbrite @@ -39,7 +40,8 @@ def scrape(self, url: str) -> ST: ... -EventScraperResult: TypeAlias = tuple[models.Event, list[models.Tag]] +ImageResult: TypeAlias = tuple[str, bytes] +EventScraperResult: TypeAlias = tuple[models.Event, list[models.Tag], ImageResult | None] class MeetupScraperMixin: @@ -84,8 +86,8 @@ def scrape(self, url: str) -> list[str]: else: upcoming_section = soup.find_all(id="upcoming-section")[0] events = upcoming_section.find_all_next(id=re.compile(r"event-card-")) - filtered_event_containers = [event for event in events if self._filter_event_tag(event)] - event_urls = [event_container["href"] for event_container in filtered_event_containers] + filtered_event_containers: list[Tag] = [event for event in events if self._filter_event_tag(event)] # type: ignore + event_urls: list[str] = [event_container["href"] for event_container in filtered_event_containers] # type: ignore return [url for url in event_urls if self._filter_repeating_events(url)] @@ -136,6 +138,8 @@ def scrape(self, url: str) -> EventScraperResult: location_data = apollo_state[event_json["venue"]["__ref"]] location = f"{location_data['address']}, {location_data['city']}, {location_data['state']}" external_id = event_json["id"] + event_photo = event_json["featuredEventPhoto"]["__ref"] + image_url = apollo_state[event_photo].get("highResUrl", apollo_state[event_photo]["baseUrl"]) except KeyError: name = self._parse_name(soup) description = self._parse_description(soup) @@ -143,20 +147,22 @@ def scrape(self, url: str) -> EventScraperResult: duration = self._parse_duration(soup) location = self._parse_location(soup) external_id = self._parse_external_id(url) + image_url = self._parse_image(soup) + + if image_url: + image_result = self._get_image(image_url) tags = self._parse_tags(soup) - return ( - models.Event( - name=name, - description=description, - date_time=date_time, - duration=duration, - location=location, - external_id=external_id, - url=url, - ), - tags, + event = models.Event( + name=name, + description=description, + date_time=date_time, + duration=duration, + location=location, + external_id=external_id, + url=url, ) + return (event, tags, image_result) def _parse_name(self, soup: BeautifulSoup) -> str: name: str = soup.find_all("h1")[0].text @@ -171,10 +177,16 @@ def _parse_description(self, soup: BeautifulSoup) -> str: return description def _parse_date_time(self, soup: BeautifulSoup) -> datetime: - return datetime.fromisoformat(soup.find_all("time")[0]["datetime"]) + time: Tag | None = soup.find_next("time") # type: ignore + if not time: + raise ValueError("could not find time") + dt: str = time["datetime"] # type: ignore + return datetime.fromisoformat(dt) def _parse_duration(self, soup: BeautifulSoup) -> timedelta: - time: Tag = soup.find_all("time")[0] + time: Tag | None = soup.find_next("time") # type: ignore + if not time: + raise ValueError("could not find time") matches = self.DURATION_PATTERN.findall(time.text) if not matches: raise ValueError("Could not find duration from:", time.text) @@ -199,6 +211,23 @@ def _parse_tags(self, soup: BeautifulSoup) -> list[models.Tag]: tags = [re.sub(r"\s+", " ", t.text) for t in tags] # Some tags have newlines & extra spaces return [models.Tag(value=t) for t in tags] + def _parse_image(self, soup: BeautifulSoup) -> str | None: + picture = soup.find(attrs={"data-testid": "event-description-image"}) + if not picture: + return None + img: Tag | None = picture.find("img") # type: ignore + if not img: + return None + src: str = img["src"] # type: ignore + return src + + def _get_image(self, image_url: str) -> ImageResult: + image_name = image_url.rsplit("/", maxsplit=1)[-1].split("?", maxsplit=1)[0] + response = requests.get(image_url, timeout=10) + response.raise_for_status() + image = response.content + return image_name, image + class EventbriteScraper(Scraper[list[EventScraperResult]]): def __init__(self, api_token: str | None = None): @@ -213,7 +242,7 @@ def scrape(self, organization_id: str) -> list[EventScraperResult]: events_and_tags = [self.map_to_event(eventbrite_event) for eventbrite_event in response["events"]] return events_and_tags - def map_to_event(self, eventbrite_event: dict) -> tuple[models.Event, list[models.Tag]]: + def map_to_event(self, eventbrite_event: dict) -> EventScraperResult: name = eventbrite_event["name"]["text"] start = datetime.fromisoformat(eventbrite_event["start"]["utc"]) end = datetime.fromisoformat(eventbrite_event["end"]["utc"]) @@ -249,7 +278,7 @@ def map_to_event(self, eventbrite_event: dict) -> tuple[models.Event, list[model # if subcategory_name: # tags.append(models.Tag(value=subcategory_name)) - return event, [] + return event, [], None @functools.lru_cache def _get_venue_location(self, venue_id: str) -> str: diff --git a/src/web/services.py b/src/web/services.py index f0e295e1..3d4639f6 100644 --- a/src/web/services.py +++ b/src/web/services.py @@ -1,6 +1,7 @@ from datetime import timedelta from typing import Protocol +from django.core.files.base import ContentFile from django.forms.models import model_to_dict from django.utils import timezone @@ -24,13 +25,15 @@ def save_events(self) -> None: for tech_group in models.TechGroup.objects.filter(homepage__icontains="meetup.com"): event_urls = self.homepage_scraper.scrape(tech_group.homepage) # type: ignore for event_url in event_urls: # TODO: parallelize (with async?) - event, tags = self.event_scraper.scrape(event_url) + event, tags, image_result = self.event_scraper.scrape(event_url) event.group = tech_group event.approved_at = now defaults = model_to_dict(event, exclude=["id"]) defaults["group"] = tech_group del defaults["tags"] # Can't apply Many-to-Many relationship untill after the event has been saved. + del defaults["image"] + new_event, _ = models.Event.objects.update_or_create( external_id=event.external_id, defaults=defaults, @@ -39,6 +42,19 @@ def save_events(self) -> None: tag, _ = models.Tag.objects.get_or_create(value=tag) new_event.tags.add(tag) + if image_result is not None: + image_name, image = image_result + + # If images are the same, don't re-upload + has_existing_image = bool(new_event.image) + if has_existing_image: + existing_image = new_event.image.read() + if existing_image == image: + continue + + file = ContentFile(image, name=image_name) + new_event.image.save(image_name, file) + class EventbriteService: events_scraper: scrapers.Scraper[list[scrapers.EventScraperResult]] @@ -58,7 +74,7 @@ def save_events(self) -> None: for eventbrite_organization in models.EventbriteOrganization.objects.prefetch_related("tech_group"): tech_group = eventbrite_organization.tech_group events_and_tags = self.events_scraper.scrape(eventbrite_organization.eventbrite_id) - for event, _ in events_and_tags: + for event, _, _ in events_and_tags: event.group = tech_group event.approved_at = now defaults = model_to_dict(event, exclude=["id"]) diff --git a/src/web/tests/data/meetup-image.jpeg b/src/web/tests/data/meetup-image.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..6141648448d0ab1e3a06d4918de8117126885651 GIT binary patch literal 51328 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 aYmNG~nE%Rpzu>{|;N<^3JgEQq?LPo!BsXXP literal 0 HcmV?d00001 diff --git a/src/web/tests/data/meetup-image.webp b/src/web/tests/data/meetup-image.webp new file mode 100644 index 0000000000000000000000000000000000000000..5b6cadfdc6aa52ae04205575ded0be91ea775cbc GIT binary patch literal 11840 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 0 + @responses.activate def test_scraper_without_json(self): + # Arrange fin = open(pathlib.Path(__file__).parent / "data" / "meetup-without-json.html") body = fin.read() fin.close() @@ -96,9 +113,20 @@ def test_scraper_without_json(self): body=body, ) + with open(pathlib.Path(__file__).parent / "data" / "meetup-image.webp", "rb") as fin: + body = fin.read() + responses.get( + "https://secure.meetupstatic.com/photos/event/1/0/a/e/600_519844270.webp?w=750", + body=body, + ) + + # Act scraper = scrapers.MeetupEventScraper() - actual, actual_tags = scraper.scrape("https://www.meetup.com/python-spokane/events/298213205/") + actual, actual_tags, actual_image_result = scraper.scrape( + "https://www.meetup.com/python-spokane/events/298213205/" + ) + # Assert assert actual.name == "Dagger with Spokane Tech 🚀" assert actual.description and actual.description.startswith("Join us for our monthly SPUG meetup!") assert actual.date_time == datetime(2024, 3, 19, 18, 0, 0, tzinfo=ZoneInfo("America/Los_Angeles")) @@ -115,6 +143,10 @@ def test_scraper_without_json(self): "Agile and Scrum", } + assert actual_image_result + assert actual_image_result[0] == "600_519844270.webp" + assert len(actual_image_result[1]) > 0 + @pytest.mark.eventbrite class TestEventbriteScraper(TestCase): From da0f6a439b2e1200b34c82b3b5b017ff2ed289e8 Mon Sep 17 00:00:00 2001 From: joeriddles Date: Fri, 13 Sep 2024 00:17:33 -0700 Subject: [PATCH 2/2] Add image scraping to EventbriteScraper --- src/web/scrapers.py | 46 +++++--- src/web/services.py | 109 +++++++++++------- .../data/eventbrite/event_description.json | 3 + src/web/tests/data/eventbrite/event_image.jpg | Bin 0 -> 26739 bytes .../tests/data/eventbrite/event_venue.json | 25 ++++ .../data/eventbrite/organizer_events.json | 83 +++++++++++++ src/web/tests/test_scrapers.py | 90 +++++++++++---- src/web/tests/test_services.py | 33 ++++++ 8 files changed, 311 insertions(+), 78 deletions(-) create mode 100644 src/web/tests/data/eventbrite/event_description.json create mode 100644 src/web/tests/data/eventbrite/event_image.jpg create mode 100644 src/web/tests/data/eventbrite/event_venue.json create mode 100644 src/web/tests/data/eventbrite/organizer_events.json diff --git a/src/web/scrapers.py b/src/web/scrapers.py index e0d41dd9..c16ebb37 100644 --- a/src/web/scrapers.py +++ b/src/web/scrapers.py @@ -27,7 +27,7 @@ def get_venue(self, id, **data): def get_event_description(self, id, **data): - return self.get("/events/{0}/description//".format(id), data=data) + return self.get("/events/{0}/description/".format(id), data=data) setattr(eventbrite.access_methods.AccessMethodsMixin, "get_venue", get_venue) @@ -44,7 +44,19 @@ def scrape(self, url: str) -> ST: EventScraperResult: TypeAlias = tuple[models.Event, list[models.Tag], ImageResult | None] -class MeetupScraperMixin: +class ScraperMixin: + def _get_image(self, image_url: str) -> ImageResult: + image_name = self._parse_image_name(image_url) + response = requests.get(image_url, timeout=10) + response.raise_for_status() + image = response.content + return image_name, image + + def _parse_image_name(self, image_url: str) -> str: + return image_url.rsplit("/", maxsplit=1)[-1].split("?", maxsplit=1)[0] + + +class MeetupScraperMixin(ScraperMixin): """Common Meetup scraping functionality.""" def _parse_apollo_state(self, soup: BeautifulSoup) -> dict: @@ -177,14 +189,14 @@ def _parse_description(self, soup: BeautifulSoup) -> str: return description def _parse_date_time(self, soup: BeautifulSoup) -> datetime: - time: Tag | None = soup.find_next("time") # type: ignore + time: Tag | None = soup.find("time") # type: ignore if not time: raise ValueError("could not find time") dt: str = time["datetime"] # type: ignore return datetime.fromisoformat(dt) def _parse_duration(self, soup: BeautifulSoup) -> timedelta: - time: Tag | None = soup.find_next("time") # type: ignore + time: Tag | None = soup.find("time") # type: ignore if not time: raise ValueError("could not find time") matches = self.DURATION_PATTERN.findall(time.text) @@ -221,15 +233,8 @@ def _parse_image(self, soup: BeautifulSoup) -> str | None: src: str = img["src"] # type: ignore return src - def _get_image(self, image_url: str) -> ImageResult: - image_name = image_url.rsplit("/", maxsplit=1)[-1].split("?", maxsplit=1)[0] - response = requests.get(image_url, timeout=10) - response.raise_for_status() - image = response.content - return image_name, image - -class EventbriteScraper(Scraper[list[EventScraperResult]]): +class EventbriteScraper(ScraperMixin, Scraper[list[EventScraperResult]]): def __init__(self, api_token: str | None = None): self.client = Eventbrite(api_token or settings.EVENTBRITE_API_TOKEN) self._location_by_venue_id: dict[str, str] = {} @@ -238,9 +243,10 @@ def scrape(self, organization_id: str) -> list[EventScraperResult]: response = self.client.get_organizer_events( organization_id, status="live", + expand="logo", ) - events_and_tags = [self.map_to_event(eventbrite_event) for eventbrite_event in response["events"]] - return events_and_tags + results = [self.map_to_event(eventbrite_event) for eventbrite_event in response["events"]] + return results def map_to_event(self, eventbrite_event: dict) -> EventScraperResult: name = eventbrite_event["name"]["text"] @@ -259,6 +265,16 @@ def map_to_event(self, eventbrite_event: dict) -> EventScraperResult: # short description description = eventbrite_event["description"]["html"] + try: + image_url = eventbrite_event["logo"]["original"]["url"] + image_result = self._get_image(image_url) + except (KeyError, requests.HTTPError): + try: + image_url = eventbrite_event["logo"]["url"] + image_result = self._get_image(image_url) + except KeyError: + image_result = None + event = models.Event( name=name, description=description, @@ -278,7 +294,7 @@ def map_to_event(self, eventbrite_event: dict) -> EventScraperResult: # if subcategory_name: # tags.append(models.Tag(value=subcategory_name)) - return event, [], None + return event, [], image_result @functools.lru_cache def _get_venue_location(self, venue_id: str) -> str: diff --git a/src/web/services.py b/src/web/services.py index 3d4639f6..9823635b 100644 --- a/src/web/services.py +++ b/src/web/services.py @@ -8,52 +8,84 @@ from web import models, scrapers +class EventService: + def save_event_from_result( + self, + result: scrapers.EventScraperResult, + tech_group: models.TechGroup, + ) -> None: + event, tags, image_result = result + event = self._save_event(event, tech_group) + self._save_tags(event, tags) + if image_result is not None: + self._save_image(event, image_result) + + def _save_event( + self, + event: models.Event, + tech_group: models.TechGroup, + ) -> models.Event: + event.group = tech_group + event.approved_at = timezone.localtime() + defaults = model_to_dict(event, exclude=["id"]) + defaults["group"] = tech_group + + del defaults["tags"] # Can't apply Many-to-Many relationship untill after the event has been saved. + del defaults["image"] + + updated_event, _ = models.Event.objects.update_or_create( + external_id=event.external_id, + defaults=defaults, + ) + return updated_event + + def _save_tags( + self, + event: models.Event, + tags: list[models.Tag], + ) -> None: + for tag in tags: + tag, _ = models.Tag.objects.get_or_create(value=tag) + event.tags.add(tag) + + def _save_image( + self, + event: models.Event, + image_result: scrapers.ImageResult, + ) -> None: + image_name, image = image_result + + # If images are the same, don't re-upload + has_existing_image = bool(event.image) + if has_existing_image: + existing_image = event.image.read() + if existing_image == image: + return + + file = ContentFile(image, name=image_name) + event.image.save(image_name, file) + + class MeetupService: def __init__( self, homepage_scraper: scrapers.Scraper[list[str]] | None = None, event_scraper: scrapers.Scraper[scrapers.EventScraperResult] | None = None, + event_service: EventService | None = None, ) -> None: self.homepage_scraper: scrapers.Scraper[list[str]] = homepage_scraper or scrapers.MeetupHomepageScraper() self.event_scraper: scrapers.Scraper[scrapers.EventScraperResult] = ( event_scraper or scrapers.MeetupEventScraper() ) + self.event_service = event_service or EventService() def save_events(self) -> None: """Scrape upcoming events from Meetup and save them to the database.""" - now = timezone.localtime() for tech_group in models.TechGroup.objects.filter(homepage__icontains="meetup.com"): event_urls = self.homepage_scraper.scrape(tech_group.homepage) # type: ignore for event_url in event_urls: # TODO: parallelize (with async?) - event, tags, image_result = self.event_scraper.scrape(event_url) - event.group = tech_group - event.approved_at = now - defaults = model_to_dict(event, exclude=["id"]) - defaults["group"] = tech_group - - del defaults["tags"] # Can't apply Many-to-Many relationship untill after the event has been saved. - del defaults["image"] - - new_event, _ = models.Event.objects.update_or_create( - external_id=event.external_id, - defaults=defaults, - ) - for tag in tags: - tag, _ = models.Tag.objects.get_or_create(value=tag) - new_event.tags.add(tag) - - if image_result is not None: - image_name, image = image_result - - # If images are the same, don't re-upload - has_existing_image = bool(new_event.image) - if has_existing_image: - existing_image = new_event.image.read() - if existing_image == image: - continue - - file = ContentFile(image, name=image_name) - new_event.image.save(image_name, file) + result = self.event_scraper.scrape(event_url) + self.event_service.save_event_from_result(result, tech_group) class EventbriteService: @@ -62,28 +94,21 @@ class EventbriteService: def __init__( self, events_scraper: scrapers.Scraper[list[scrapers.EventScraperResult]] | None = None, + event_service: EventService | None = None, ) -> None: self.events_scraper = events_scraper or scrapers.EventbriteScraper() + self.event_service = event_service or EventService() def save_events(self) -> None: """Fetch upcoming events from Eventbrite and save them. Note: this uses an API and doesn't actually web scrape. """ - now = timezone.localtime() for eventbrite_organization in models.EventbriteOrganization.objects.prefetch_related("tech_group"): tech_group = eventbrite_organization.tech_group - events_and_tags = self.events_scraper.scrape(eventbrite_organization.eventbrite_id) - for event, _, _ in events_and_tags: - event.group = tech_group - event.approved_at = now - defaults = model_to_dict(event, exclude=["id"]) - defaults["group"] = tech_group - del defaults["tags"] # Can't apply Many-to-Many relationship untill after the event has been saved. - models.Event.objects.update_or_create( - external_id=event.external_id, - defaults=defaults, - ) + results = self.events_scraper.scrape(eventbrite_organization.eventbrite_id) + for result in results: + self.event_service.save_event_from_result(result, tech_group) class Sender(Protocol): diff --git a/src/web/tests/data/eventbrite/event_description.json b/src/web/tests/data/eventbrite/event_description.json new file mode 100644 index 00000000..1e7a1d2c --- /dev/null +++ b/src/web/tests/data/eventbrite/event_description.json @@ -0,0 +1,3 @@ +{ + "description": "
Full Day of Panels, Speakers and Vendors on Cybersecurity, AI and Compliance. FREE with pre-registration - space limited - Oct 2, 2024" +} diff --git a/src/web/tests/data/eventbrite/event_image.jpg b/src/web/tests/data/eventbrite/event_image.jpg new file mode 100644 index 0000000000000000000000000000000000000000..caad0c37a74051920c0012dbcbed64aafe66229e GIT binary patch literal 26739 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 literal 0 HcmV?d00001 diff --git a/src/web/tests/data/eventbrite/event_venue.json b/src/web/tests/data/eventbrite/event_venue.json new file mode 100644 index 00000000..cf5a8a93 --- /dev/null +++ b/src/web/tests/data/eventbrite/event_venue.json @@ -0,0 +1,25 @@ +{ + "address": { + "address_1": "702 East Desmet Avenue", + "address_2": "", + "city": "Spokane", + "region": "WA", + "postal_code": "99202", + "country": "US", + "latitude": "47.6672448", + "longitude": "-117.3999126", + "localized_address_display": "702 East Desmet Avenue, Spokane, WA 99202", + "localized_area_display": "Spokane, WA", + "localized_multi_line_address_display": [ + "702 East Desmet Avenue", + "Spokane, WA 99202" + ] + }, + "resource_uri": "https://www.eventbriteapi.com/v3/venues/214450569/", + "id": "214450569", + "age_restriction": null, + "capacity": null, + "name": "John J. Hemmingson Center", + "latitude": "47.6672448", + "longitude": "-117.3999126" +} diff --git a/src/web/tests/data/eventbrite/organizer_events.json b/src/web/tests/data/eventbrite/organizer_events.json new file mode 100644 index 00000000..400df1f6 --- /dev/null +++ b/src/web/tests/data/eventbrite/organizer_events.json @@ -0,0 +1,83 @@ +{ + "pagination": { + "object_count": 1, + "page_number": 1, + "page_size": 50, + "page_count": 1, + "has_more_items": false + }, + "events": [ + { + "name": { + "text": "3rd Annual - INCH360 Regional Cybersecurity Conference", + "html": "3rd Annual - INCH360 Regional Cybersecurity Conference" + }, + "description": { + "text": "Full Day of Panels, Speakers and Vendors on Cybersecurity, AI and Compliance. FREE with pre-registration - space limited - Oct 2, 2024", + "html": "Full Day of Panels, Speakers and Vendors on Cybersecurity, AI and Compliance. FREE with pre-registration - space limited - Oct 2, 2024" + }, + "url": "https://www.eventbrite.com/e/3rd-annual-inch360-regional-cybersecurity-conference-tickets-909447069667", + "start": { + "timezone": "America/Los_Angeles", + "local": "2024-10-02T08:30:00", + "utc": "2024-10-02T15:30:00Z" + }, + "end": { + "timezone": "America/Los_Angeles", + "local": "2024-10-02T16:00:00", + "utc": "2024-10-02T23:00:00Z" + }, + "organization_id": "1773924472233", + "created": "2024-05-19T22:29:10Z", + "changed": "2024-09-06T16:49:53Z", + "published": "2024-05-20T20:03:26Z", + "capacity": null, + "capacity_is_custom": null, + "status": "live", + "currency": "USD", + "listed": true, + "shareable": true, + "online_event": false, + "tx_time_limit": 1200, + "hide_start_date": false, + "hide_end_date": false, + "locale": "en_US", + "is_locked": false, + "privacy_setting": "unlocked", + "is_series": false, + "is_series_parent": false, + "inventory_type": "limited", + "is_reserved_seating": false, + "show_pick_a_seat": false, + "show_seatmap_thumbnail": false, + "show_colors_in_seatmap_thumbnail": false, + "source": "auto_create", + "is_free": true, + "version": null, + "summary": "Full Day of Panels, Speakers and Vendors on Cybersecurity, AI and Compliance. FREE with pre-registration - space limited - Oct 2, 2024", + "facebook_event_id": null, + "logo_id": "843746309", + "organizer_id": "72020528223", + "venue_id": "214450569", + "category_id": "102", + "subcategory_id": "2004", + "format_id": "2", + "id": "909447069667", + "resource_uri": "https://www.eventbriteapi.com/v3/events/909447069667/", + "is_externally_ticketed": false, + "logo": { + "crop_mask": null, + "original": { + "url": "https://img.evbuc.com/https%3A%2F%2Fcdn.evbuc.com%2Fimages%2F843746309%2F530357704049%2F1%2Foriginal.20240906-164727?auto=format%2Ccompress&q=75&sharp=10&s=09370c02bd3ab62907337f2e1ca8a61d", + "width": 6912, + "height": 3456 + }, + "id": "843746309", + "url": "https://img.evbuc.com/https%3A%2F%2Fcdn.evbuc.com%2Fimages%2F843746309%2F530357704049%2F1%2Foriginal.20240906-164727?h=200&w=450&auto=format%2Ccompress&q=75&sharp=10&s=4b5f5340dcfe0cc78bec9f19b08f45f7", + "aspect_ratio": "2", + "edge_color": "#516c79", + "edge_color_set": true + } + } + ] +} diff --git a/src/web/tests/test_scrapers.py b/src/web/tests/test_scrapers.py index 13db1da5..5839e836 100644 --- a/src/web/tests/test_scrapers.py +++ b/src/web/tests/test_scrapers.py @@ -7,7 +7,27 @@ import responses from django.test import TestCase -from web import models, scrapers +from web import scrapers + +BASE_DATA_DIR = pathlib.Path(__file__).parent / "data" + + +def mock_response( + url: str, + filepath: pathlib.Path, +) -> None: + with open(filepath) as fin: + body = fin.read() + responses.get(url, body=body) + + +def mock_image_response( + url: str, + filepath: pathlib.Path, +) -> None: + with open(filepath, "rb") as fin: + body = fin.read() + responses.get(url, body=body) class TestMeetupHomepageScraper(TestCase): @@ -105,19 +125,13 @@ def test_scraper_with_json(self): @responses.activate def test_scraper_without_json(self): # Arrange - fin = open(pathlib.Path(__file__).parent / "data" / "meetup-without-json.html") - body = fin.read() - fin.close() - responses.get( + mock_response( "https://www.meetup.com/python-spokane/events/298213205/", - body=body, + BASE_DATA_DIR / "meetup-without-json.html", ) - - with open(pathlib.Path(__file__).parent / "data" / "meetup-image.webp", "rb") as fin: - body = fin.read() - responses.get( + mock_image_response( "https://secure.meetupstatic.com/photos/event/1/0/a/e/600_519844270.webp?w=750", - body=body, + BASE_DATA_DIR / "meetup-image.webp", ) # Act @@ -158,16 +172,50 @@ class TestEventbriteScraper(TestCase): To run them, set the `EVENTBRITE_API_TOKEN` envrionment variable. """ + @responses.activate def test_scraper(self): + # Arrange + mock_response( + "https://www.eventbriteapi.com/v3/organizers/72020528223/events/", + BASE_DATA_DIR / "eventbrite" / "organizer_events.json", + ) + mock_response( + "https://www.eventbriteapi.com/v3/venues/214450569/?expand=none", + BASE_DATA_DIR / "eventbrite" / "event_venue.json", + ) + mock_response( + "https://www.eventbriteapi.com/v3/events/909447069667/description/", + BASE_DATA_DIR / "eventbrite" / "event_description.json", + ) + mock_image_response( + "https://img.evbuc.com/https%3A%2F%2Fcdn.evbuc.com%2Fimages%2F843746309%2F530357704049%2F1%2Foriginal.20240906-164727?auto=format%2Ccompress&q=75&sharp=10&s=09370c02bd3ab62907337f2e1ca8a61d", + BASE_DATA_DIR / "eventbrite" / "event_image.jpg", + ) + + # Act scraper = scrapers.EventbriteScraper() - result = scraper.scrape("72020528223") - actual: models.Event = result[0][0] - assert actual.name == "Spring Cyber - Training Series" - assert actual.description and actual.description.startswith( - "
Deep Dive into Pen Testing with white hacker Casey Davis" + organization_id = "72020528223" + result = scraper.scrape(organization_id) + + # Assert + event, tags, image_result = result[0] + assert event.name == "3rd Annual - INCH360 Regional Cybersecurity Conference" + assert event.description + assert event.description.startswith("
Full Day of Panels, Speakers and Vendors on Cybersecurity,") + assert event.date_time == datetime(2024, 10, 2, 8, 30, 0, tzinfo=ZoneInfo("America/Los_Angeles")) + assert event.duration == timedelta(hours=7, minutes=30) + assert event.location == "702 East Desmet Avenue, Spokane, WA 99202" + assert ( + event.url + == "https://www.eventbrite.com/e/3rd-annual-inch360-regional-cybersecurity-conference-tickets-909447069667" ) - assert actual.date_time == datetime(2024, 5, 23, 16, 0, 0, tzinfo=ZoneInfo("America/Los_Angeles")) - assert actual.duration == timedelta(hours=1, minutes=30) - assert actual.location == "2818 North Sullivan Road #Suite 100, Spokane Valley, WA 99216" - assert actual.url == "https://www.eventbrite.com/e/spring-cyber-training-series-tickets-860181354587" - assert actual.external_id == "860181354587" + assert event.external_id == "909447069667" + + assert not tags + + assert image_result + assert ( + image_result[0] + == "https%3A%2F%2Fcdn.evbuc.com%2Fimages%2F843746309%2F530357704049%2F1%2Foriginal.20240906-164727" + ) + assert len(image_result[1]) > 0 diff --git a/src/web/tests/test_services.py b/src/web/tests/test_services.py index b1a3e9de..836b6e8a 100644 --- a/src/web/tests/test_services.py +++ b/src/web/tests/test_services.py @@ -1,5 +1,6 @@ from django.test import TestCase from django.utils import timezone + from web import models, scrapers, services @@ -32,6 +33,10 @@ def scrape(self, url: str) -> scrapers.EventScraperResult: models.Tag(value="Agile and Scrum"), models.Tag(value="Python Web Development"), ], + ( + "image_name", + b"image.png", + ), ) return ( @@ -48,6 +53,10 @@ def scrape(self, url: str) -> scrapers.EventScraperResult: models.Tag(value="Agile and Scrum"), models.Tag(value="Python Web Development"), ], + ( + "image_name", + b"image.png", + ), ) @@ -75,6 +84,30 @@ def test_updates_event_instead_of_creating_new_one(self): assert event.name == "Intro to Dagger" assert event.description == "Super cool intro to Dagger CI/CD!" assert event.external_id == MockMeetupEventScraper.EXTERNAL_ID + assert "image_name" in event.image.name + + def test_image_is_not_reuploaded_when_contents_are_same(self): + # Arrange + models.TechGroup.objects.create( + name="Spokane Python User Group", + homepage="https://www.meetup.com/Python-Spokane/", + ) + + meetup_service = services.MeetupService( + MockMeetupHomepageScraper(), + MockMeetupEventScraper(), + ) + + # Act + meetup_service.save_events() + event1 = models.Event.objects.get() + + meetup_service.save_events() + event2 = models.Event.objects.get() + + # Assert + assert event1.pk == event2.pk + assert event1.image.name == event2.image.name def test_manually_applied_tags_are_not_overriden(self): # Arrange