diff --git a/.github/workflows/bandit.yaml b/.github/workflows/bandit.yaml index 7f52e3c..6241ca7 100644 --- a/.github/workflows/bandit.yaml +++ b/.github/workflows/bandit.yaml @@ -9,10 +9,11 @@ jobs: bandit: runs-on: ubuntu-latest name: "bandit" + if: github.event.created == false # Skip if this push created a new branch steps: - uses: davidslusser/actions_python_bandit@v1.0.1 with: src: "src" options: "-c pyproject.toml -r" pip_install_command: "pip install .[dev]" - python_version: "3.11" + python_version: "3.13" diff --git a/.github/workflows/fawltydeps.yaml b/.github/workflows/fawltydeps.yaml index cbd1d42..4f709ad 100644 --- a/.github/workflows/fawltydeps.yaml +++ b/.github/workflows/fawltydeps.yaml @@ -12,7 +12,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-python@v4 with: - python-version: "3.11" + python-version: "3.13" - name: Install dependencies run: | pip install -e .[dev] diff --git a/.github/workflows/isort.yaml b/.github/workflows/isort.yaml index f7564e3..5b52f95 100644 --- a/.github/workflows/isort.yaml +++ b/.github/workflows/isort.yaml @@ -9,9 +9,10 @@ jobs: isort: runs-on: ubuntu-latest name: "isort" + if: github.event.created == false # Skip if this push created a new branch steps: - uses: davidslusser/actions_python_isort@v1.0.1 with: src: "src/django_project" options: "--check --diff" - python_version: "3.11" + python_version: "3.13" diff --git a/.github/workflows/mypy.yaml b/.github/workflows/mypy.yaml index 1da7730..ac392b3 100644 --- a/.github/workflows/mypy.yaml +++ b/.github/workflows/mypy.yaml @@ -9,9 +9,11 @@ jobs: mypy: runs-on: ubuntu-latest name: "mypy" + if: github.event.created == false # Skip if this push created a new branch steps: - uses: davidslusser/actions_python_mypy@v1.0.1 with: src: "src" - options: "-v" + options: "" pip_install_command: "pip install -e .[dev]" + python_version: "3.13" diff --git a/.github/workflows/pytest.yaml b/.github/workflows/pytest.yaml index d432e08..5ada98d 100644 --- a/.github/workflows/pytest.yaml +++ b/.github/workflows/pytest.yaml @@ -14,4 +14,4 @@ jobs: src: "" options: "" pip_install_command: "pip install -e .[dev]" - python_version: "3.11" + python_version: "3.13" diff --git a/.github/workflows/radon.yaml b/.github/workflows/radon.yaml index 8277e48..516014c 100644 --- a/.github/workflows/radon.yaml +++ b/.github/workflows/radon.yaml @@ -9,6 +9,7 @@ jobs: radon: runs-on: ubuntu-latest name: "radon" + if: github.event.created == false # Skip if this push created a new branch steps: - uses: actions/checkout@v3 - uses: davidslusser/actions_python_radon@v1.0.0 diff --git a/.github/workflows/ruff_format.yaml b/.github/workflows/ruff_format.yaml index 8bfcd67..ea1f7fb 100644 --- a/.github/workflows/ruff_format.yaml +++ b/.github/workflows/ruff_format.yaml @@ -9,10 +9,11 @@ jobs: ruff: runs-on: ubuntu-latest name: "ruff" + if: github.event.created == false # Skip if this push created a new branch steps: - name: actions_python_ruff uses: davidslusser/actions_python_ruff@v1.0.3 with: src: "src/django_project" command: ruff format src --check - python_version: "3.11" + python_version: "3.13" diff --git a/.github/workflows/ruff_lint.yaml b/.github/workflows/ruff_lint.yaml index 9728d31..6d6545b 100644 --- a/.github/workflows/ruff_lint.yaml +++ b/.github/workflows/ruff_lint.yaml @@ -9,10 +9,11 @@ jobs: ruff: runs-on: ubuntu-latest name: "ruff" + if: github.event.created == false # Skip if this push created a new branch steps: - name: actions_python_ruff uses: davidslusser/actions_python_ruff@v1.0.3 with: src: "src/django_project" options: "-v" - python_version: "3.11" + python_version: "3.13" diff --git a/src/django_project/web/utilities/scrapers/meetup.py b/src/django_project/web/utilities/scrapers/meetup.py index 63d6207..989789d 100644 --- a/src/django_project/web/utilities/scrapers/meetup.py +++ b/src/django_project/web/utilities/scrapers/meetup.py @@ -13,7 +13,7 @@ def get_end_datetime(datetime_string: str, time_string: str) -> datetime | None: Args: datetime_string (str): string representation of a datetime; example: '2025-01-06T07:00:00-08:00' - time_string (str): string representation of a time; example: '8:00 AM PST' + time_string (str): string representation of a time; example: '7:00 PM PST' Returns: datetime: datetime object with timezone information @@ -28,9 +28,21 @@ def get_end_datetime(datetime_string: str, time_string: str) -> datetime | None: offset = timedelta(hours=offset_hours, minutes=offset_minutes) tz = timezone(offset) - # Parse the time string - time_part: str = time_string.split(" ")[0].strip() # Get the time part - time_obj: datetime = datetime.strptime(time_part.replace(" ", ""), "%I:%M %p") # Parse 12-hour format + # Parse the time string using regex to extract the time part + match = re.search(r"(\d{1,2}):(\d{2})\s*([aApP][mM])", time_string) + if not match: + return None + hour = int(match.group(1)) + minute = int(match.group(2)) + period = match.group(3).upper() + + # Convert 12-hour format to 24-hour format + if period == "PM" and hour != 12: + hour += 12 + elif period == "AM" and hour == 12: + hour = 0 + + time_obj = datetime.min.replace(hour=hour, minute=minute) # Combine date and time into a new datetime object combined_datetime: datetime = datetime.combine(datetime.strptime(date_part, "%Y-%m-%d").date(), time_obj.time()) @@ -72,25 +84,78 @@ def get_event_information(url: str) -> dict: if isinstance(description_div, Tag): # Type check for Tag event_info["description"] = "".join(str(child) for child in description_div.children) - time_element: PageElement | Tag | NavigableString | None = soup.find("time") + time_element: PageElement | Tag | NavigableString | None = soup.find("time", class_="block") if time_element: if isinstance(time_element, Tag): # Check if time_element is a Tag start_time_string: str | AttributeValueList | None = time_element.get("datetime", None) time_text: str = time_element.get_text(separator=" ").strip() - end_time_string: str = time_text.split(" to ")[-1] if start_time_string: if isinstance(start_time_string, str): # Check if start_time_string is a str - event_info["start_datetime"] = datetime.fromisoformat(start_time_string) - event_info["end_datetime"] = get_end_datetime(start_time_string, end_time_string) + start_dt = datetime.fromisoformat(start_time_string) + event_info["start_datetime"] = start_dt + + # Parse duration from the time text which shows times in UTC + # Format: "Friday, Feb 13 ยท 2:00 AM to 3:00 AM UTC" + # We calculate the duration and add it to start_dt to preserve timezone + if " to " in time_text: + time_parts = time_text.split(" to ") + if len(time_parts) == 2: + # Extract start time from text (in UTC) + start_match = re.search(r"(\d{1,2}):(\d{2})\s*([APap][Mm])", time_parts[0]) + # Extract end time from text (in UTC) + end_match = re.search(r"(\d{1,2}):(\d{2})\s*([APap][Mm])", time_parts[1]) + + if start_match and end_match: + # Parse start time (UTC) + start_hour = int(start_match.group(1)) + start_minute = int(start_match.group(2)) + start_period = start_match.group(3).upper() + if start_period == "PM" and start_hour != 12: + start_hour += 12 + elif start_period == "AM" and start_hour == 12: + start_hour = 0 + + # Parse end time (UTC) + end_hour = int(end_match.group(1)) + end_minute = int(end_match.group(2)) + end_period = end_match.group(3).upper() + if end_period == "PM" and end_hour != 12: + end_hour += 12 + elif end_period == "AM" and end_hour == 12: + end_hour = 0 + + # Calculate duration in minutes + start_minutes = start_hour * 60 + start_minute + end_minutes = end_hour * 60 + end_minute + + # Handle overnight events + if end_minutes <= start_minutes: + end_minutes += 24 * 60 + + duration_minutes = end_minutes - start_minutes + + # Add duration to start_dt to get end_dt in the same timezone + end_dt = start_dt + timedelta(minutes=duration_minutes) + event_info["end_datetime"] = end_dt + else: + event_info["end_datetime"] = None + else: + event_info["end_datetime"] = None + else: + event_info["end_datetime"] = None location_name: str | Any = None match = re.search(r'"__typename":"Venue","id":"\d+","name":"([^"]+)"', page_content) if match: location_name = match.group(1) + if not location_name: + online_p = soup.find("p", class_="ds2-k16 text-ds2-text-fill-primary-enabled") + if online_p and online_p.get_text(strip=True) == "Online event": + location_name = "Online event" event_info["location_name"] = location_name - location_address: str | Any = None + location_address: str = "" address_match: re.Match[str] | None = re.search( r'"__typename":"Venue","id":"\d+","name":"[^"]+","address":"([^"]+)","city":"([^"]+)","state":"([^"]+)","country":"([^"]+)"', page_content, @@ -103,7 +168,7 @@ def get_event_information(url: str) -> dict: location_address = f"{street}, {city}, {state}, {country.upper()}" event_info["location_address"] = location_address - map_link: str | Any = None + map_link: str = "" map_link_match: re.Match[str] | None = re.search( r']*data-testid="map-link"[^>]*href="([^"]+)"', page_content )