Skip to content

Commit 375a280

Browse files
committed
Per comments - moving logic to audit from githubclient & moving tests
1 parent b5f0d70 commit 375a280

File tree

4 files changed

+214
-227
lines changed

4 files changed

+214
-227
lines changed

nodestream_github/audit.py

Lines changed: 87 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,10 @@
66
"""
77

88
from collections.abc import AsyncGenerator
9+
from datetime import UTC, datetime, timedelta
910
from typing import Any
1011

12+
from dateutil.relativedelta import relativedelta
1113
from nodestream.pipeline import Extractor
1214

1315
from .client import GithubRestApiClient
@@ -17,6 +19,77 @@
1719
logger = get_plugin_logger(__name__)
1820

1921

22+
def generate_date_range(lookback_period: dict[str, int]) -> list[str]:
23+
"""
24+
Generate a list of date strings in YYYY-MM-DD format for
25+
the given lookback period.
26+
"""
27+
if not lookback_period:
28+
return []
29+
30+
end_date = datetime.now(tz=UTC).date()
31+
start_date = (datetime.now(tz=UTC) - relativedelta(**lookback_period)).date()
32+
33+
delta_days = (end_date - start_date).days + 1
34+
return [
35+
(start_date + timedelta(days=i)).strftime("%Y-%m-%d") for i in range(delta_days)
36+
]
37+
38+
39+
def build_search_phrase(
40+
actions: list[str],
41+
actors: list[str],
42+
exclude_actors: list[str],
43+
target_date: str | None = None,
44+
) -> str:
45+
# adding action-based filtering
46+
actions_phrase = ""
47+
if actions:
48+
actions_phrase = " ".join(f"action:{action}" for action in actions)
49+
50+
# adding date-based filtering for a specific date
51+
date_filter = f"created:{target_date}" if target_date else ""
52+
53+
# adding actor-based filtering
54+
actors_phrase = ""
55+
if actors:
56+
actors_phrase = " ".join(f"actor:{actor}" for actor in actors)
57+
58+
# adding exclude_actors based filtering
59+
exclude_actors_phrase = ""
60+
if exclude_actors:
61+
exclude_actors_phrase = " ".join(f"-actor:{actor}" for actor in exclude_actors)
62+
return " ".join(
63+
section
64+
for section in [
65+
actions_phrase,
66+
date_filter,
67+
actors_phrase,
68+
exclude_actors_phrase,
69+
]
70+
if section
71+
).strip()
72+
73+
74+
def validate_lookback_period(lookback_period: dict[str, int]) -> dict[str, int]:
75+
"""Sanitize the lookback period to only include valid keys."""
76+
77+
def validate_positive_int(value: int) -> int:
78+
converted = int(value)
79+
if converted <= 0:
80+
negative_value_exception_msg = (
81+
f"Lookback period values must be positive: {value}"
82+
)
83+
raise ValueError(negative_value_exception_msg)
84+
return converted
85+
86+
try:
87+
return {k: validate_positive_int(v) for k, v in lookback_period.items()}
88+
except Exception as e:
89+
exception_msg = "Formatting lookback period failed"
90+
raise ValueError(exception_msg) from e
91+
92+
2093
class GithubAuditLogExtractor(Extractor):
2194
"""
2295
Extracts audit logs from the GitHub REST API.
@@ -46,12 +119,17 @@ def __init__(
46119
self.exclude_actors = exclude_actors
47120

48121
async def extract_records(self) -> AsyncGenerator[GithubAuditLog]:
49-
async for audit in self.client.fetch_enterprise_audit_log(
50-
self.enterprise_name,
51-
self.actions,
52-
self.actors,
53-
self.exclude_actors,
54-
self.lookback_period,
55-
):
56-
audit["timestamp"] = audit.pop("@timestamp")
57-
yield audit
122+
dates = generate_date_range(self.lookback_period) or [None]
123+
for target_date in dates:
124+
search_phrase = build_search_phrase(
125+
actions=self.actions,
126+
actors=self.actors,
127+
exclude_actors=self.exclude_actors,
128+
target_date=target_date,
129+
)
130+
async for audit in self.client.fetch_enterprise_audit_log(
131+
self.enterprise_name,
132+
search_phrase,
133+
):
134+
audit["timestamp"] = audit.pop("@timestamp")
135+
yield audit

nodestream_github/client/githubclient.py

Lines changed: 7 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,10 @@
66
import json
77
import logging
88
from collections.abc import AsyncGenerator
9-
from datetime import UTC, datetime, timedelta
109
from enum import Enum
1110
from typing import Any
1211

1312
import httpx
14-
from dateutil.relativedelta import relativedelta
1513
from limits import RateLimitItem, RateLimitItemPerMinute
1614
from limits.aio.storage import MemoryStorage
1715
from limits.aio.strategies import MovingWindowRateLimiter, RateLimiter
@@ -74,77 +72,6 @@ def _fetch_problem(title: str, e: httpx.HTTPError):
7472
logger.warning("Problem fetching %s", title, exc_info=e, stacklevel=2)
7573

7674

77-
def validate_lookback_period(lookback_period: dict[str, int]) -> dict[str, int]:
78-
"""Sanitize the lookback period to only include valid keys."""
79-
80-
def validate_positive_int(value: int) -> int:
81-
converted = int(value)
82-
if converted <= 0:
83-
negative_value_exception_msg = (
84-
f"Lookback period values must be positive: {value}"
85-
)
86-
raise ValueError(negative_value_exception_msg)
87-
return converted
88-
89-
try:
90-
return {k: validate_positive_int(v) for k, v in lookback_period.items()}
91-
except Exception as e:
92-
exception_msg = "Formatting lookback period failed"
93-
raise ValueError(exception_msg) from e
94-
95-
96-
def generate_date_range(lookback_period: dict[str, int]) -> list[str]:
97-
"""
98-
Generate a list of date strings in YYYY-MM-DD format for
99-
the given lookback period.
100-
"""
101-
if not lookback_period:
102-
return []
103-
104-
end_date = datetime.now(tz=UTC).date()
105-
start_date = (datetime.now(tz=UTC) - relativedelta(**lookback_period)).date()
106-
107-
delta_days = (end_date - start_date).days + 1
108-
return [
109-
(start_date + timedelta(days=i)).strftime("%Y-%m-%d") for i in range(delta_days)
110-
]
111-
112-
113-
def build_search_phrase(
114-
actions: list[str],
115-
actors: list[str],
116-
exclude_actors: list[str],
117-
target_date: str | None = None,
118-
) -> str:
119-
# adding action-based filtering
120-
actions_phrase = ""
121-
if actions:
122-
actions_phrase = " ".join(f"action:{action}" for action in actions)
123-
124-
# adding date-based filtering for a specific date
125-
date_filter = f"created:{target_date}" if target_date else ""
126-
127-
# adding actor-based filtering
128-
actors_phrase = ""
129-
if actors:
130-
actors_phrase = " ".join(f"actor:{actor}" for actor in actors)
131-
132-
# adding exclude_actors based filtering
133-
exclude_actors_phrase = ""
134-
if exclude_actors:
135-
exclude_actors_phrase = " ".join(f"-actor:{actor}" for actor in exclude_actors)
136-
return " ".join(
137-
section
138-
for section in [
139-
actions_phrase,
140-
date_filter,
141-
actors_phrase,
142-
exclude_actors_phrase,
143-
]
144-
if section
145-
).strip()
146-
147-
14875
class GithubRestApiClient:
14976
def __init__(
15077
self,
@@ -411,30 +338,17 @@ async def fetch_all_organizations(self) -> AsyncGenerator[types.GithubOrg]:
411338
async def fetch_enterprise_audit_log(
412339
self,
413340
enterprise_name: str,
414-
actions: list[str],
415-
actors: list[str],
416-
exclude_actors: list[str],
417-
lookback_period: dict[str, int],
341+
search_phrase: str | None = None,
418342
) -> AsyncGenerator[types.GithubAuditLog]:
419343
"""Fetches enterprise-wide audit log data
420-
421-
https://docs.github.com/en/enterprise-cloud@latest/rest/enterprise-admin/audit-log?apiVersion=2022-11-28#get-the-audit-log-for-an-enterprise
344+
https://docs.github.com/en/[email protected]/rest/enterprise-admin/audit-log?apiVersion=2022-11-28#get-the-audit-log-for-an-enterprise
422345
"""
423346
try:
424-
dates = generate_date_range(lookback_period) or [None]
425-
426-
for target_date in dates:
427-
search_phrase = build_search_phrase(
428-
actions=actions,
429-
actors=actors,
430-
exclude_actors=exclude_actors,
431-
target_date=target_date,
432-
)
433-
params = {"phrase": search_phrase} if search_phrase else {}
434-
async for audit in self._get_paginated(
435-
f"enterprises/{enterprise_name}/audit-log", params=params
436-
):
437-
yield audit
347+
params = {"phrase": search_phrase} if search_phrase else {}
348+
async for audit in self._get_paginated(
349+
f"enterprises/{enterprise_name}/audit-log", params=params
350+
):
351+
yield audit
438352
except httpx.HTTPError as e:
439353
_fetch_problem("audit log", e)
440354

tests/client/test_githubclient.py

Lines changed: 0 additions & 123 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,6 @@
66
from nodestream_github.client.githubclient import (
77
GithubRestApiClient,
88
RateLimitedError,
9-
generate_date_range,
10-
validate_lookback_period,
119
)
1210
from tests.mocks.githubrest import DEFAULT_BASE_URL, DEFAULT_HOSTNAME
1311

@@ -133,124 +131,3 @@ async def test_pagination_truncate_warning(
133131
def test_all_null_args():
134132
# noinspection PyTypeChecker
135133
assert GithubRestApiClient(auth_token=None, github_hostname=None)
136-
137-
138-
# Test generate_date_range
139-
140-
141-
def test_generate_date_range_empty_lookback_period():
142-
result = generate_date_range({})
143-
assert result == []
144-
145-
146-
@freeze_time("2025-08-01")
147-
def test_generate_date_range_days_only():
148-
result = generate_date_range({"days": 3})
149-
expected = ["2025-07-29", "2025-07-30", "2025-07-31", "2025-08-01"]
150-
assert result == expected
151-
152-
153-
@freeze_time("2025-08-01")
154-
def test_generate_date_range_zero_days():
155-
"""Test with zero days (same day only)."""
156-
result = generate_date_range({"days": 0})
157-
expected = ["2025-08-01"]
158-
assert result == expected
159-
160-
161-
@freeze_time("2025-08-01")
162-
def test_generate_date_range_months_only():
163-
result = generate_date_range({"months": 1})
164-
# July 1 to August 1 = 32 days
165-
assert len(result) == 32
166-
assert result[0] == "2025-07-01"
167-
assert result[-1] == "2025-08-01"
168-
169-
170-
@freeze_time("2025-08-01")
171-
def test_generate_date_range_years_only():
172-
result = generate_date_range({"years": 1})
173-
assert len(result) == 366
174-
assert result[0] == "2024-08-01"
175-
assert result[-1] == "2025-08-01"
176-
177-
178-
@freeze_time("2025-08-01")
179-
def test_generate_date_range_combined_periods():
180-
"""Test with combined periods."""
181-
result = generate_date_range({"months": 1, "days": 5})
182-
assert result[0] == "2025-06-26"
183-
assert result[-1] == "2025-08-01"
184-
185-
186-
@freeze_time("2025-08-01")
187-
def test_generate_date_range_complex_combination():
188-
"""Test with years, months, and days."""
189-
result = generate_date_range({"years": 1, "months": 2, "days": 10})
190-
assert result[0] == "2024-05-22"
191-
assert result[-1] == "2025-08-01"
192-
193-
194-
# Test validate_lookback_period
195-
196-
197-
def test_validate_lookback_period_valid_input():
198-
result = validate_lookback_period({"days": 7, "months": 2, "years": 1})
199-
expected = {"days": 7, "months": 2, "years": 1}
200-
assert result == expected
201-
202-
203-
def test_validate_lookback_period_empty_dict():
204-
result = validate_lookback_period({})
205-
assert result == {}
206-
207-
208-
def test_validate_lookback_period_single_value():
209-
result = validate_lookback_period({"days": 30})
210-
assert result == {"days": 30}
211-
212-
213-
def test_validate_lookback_period_string_to_int_conversion():
214-
result = validate_lookback_period({"days": "7", "months": "2"})
215-
expected = {"days": 7, "months": 2}
216-
assert result == expected
217-
218-
219-
def test_validate_lookback_period_zero_value():
220-
with pytest.raises(ValueError, match="Formatting lookback period failed"):
221-
validate_lookback_period({"days": 0})
222-
223-
224-
def test_validate_lookback_period_negative_value():
225-
with pytest.raises(ValueError, match="Formatting lookback period failed"):
226-
validate_lookback_period({"days": -5})
227-
228-
229-
def test_validate_lookback_period_multiple_negative_values():
230-
with pytest.raises(ValueError, match="Formatting lookback period failed"):
231-
validate_lookback_period({"days": 7, "months": -1, "years": 2})
232-
233-
234-
def test_validate_lookback_period_zero_string():
235-
with pytest.raises(ValueError, match="Formatting lookback period failed"):
236-
validate_lookback_period({"days": "0"})
237-
238-
239-
def test_validate_lookback_period_negative_string():
240-
with pytest.raises(ValueError, match="Formatting lookback period failed"):
241-
validate_lookback_period({"months": "-10"})
242-
243-
244-
def test_validate_lookback_period_invalid_string():
245-
with pytest.raises(ValueError, match="Formatting lookback period failed"):
246-
validate_lookback_period({"days": "invalid"})
247-
248-
249-
def test_validate_lookback_period_invalid_type():
250-
with pytest.raises(ValueError, match="Formatting lookback period failed"):
251-
validate_lookback_period({"days": []})
252-
253-
254-
def test_validate_lookback_period_none_value():
255-
with pytest.raises(ValueError, match="Formatting lookback period failed"):
256-
validate_lookback_period({"days": None})

0 commit comments

Comments
 (0)