Skip to content

Commit 899ee72

Browse files
committed
feat: enhance affiliation service with new status and refactor logging
Signed-off-by: Yeganathan S <63534555+skwowet@users.noreply.github.com>
1 parent 99cd758 commit 899ee72

2 files changed

Lines changed: 79 additions & 54 deletions

File tree

services/apps/git_integration/src/crowdgit/enums.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ class ExecutionStatus(str, Enum):
7373
class AffiliationRegistryStatus(str, Enum):
7474
SUCCESS = "success"
7575
NOT_FOUND = "not_found"
76+
UNUSABLE = "unusable"
7677
ERROR = "error"
7778

7879

@@ -82,6 +83,6 @@ class OperationType(str, Enum):
8283
CLONE = "Clone"
8384
COMMIT = "Commit"
8485
MAINTAINER = "Maintainer"
85-
REPO_AFFILIATION = "RepoAffiliation"
86+
AFFILIATION = "Affiliation"
8687
SOFTWARE_VALUE = "SoftwareValue"
8788
VULNERABILITY_SCAN = "VulnerabilityScanner"

services/apps/git_integration/src/crowdgit/services/affiliation/affiliation_service.py

Lines changed: 77 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -272,14 +272,9 @@ async def pick_affiliation_file_with_ai(
272272

273273
total_cost = 0.0
274274
batch_size = self.FILE_PICKER_BATCH_SIZE
275-
total_batches = (len(candidates) + batch_size - 1) // batch_size
276275

277-
for batch_index, batch_start in enumerate(range(0, len(candidates), batch_size), start=1):
276+
for batch_start in range(0, len(candidates), batch_size):
278277
batch = candidates[batch_start : batch_start + batch_size]
279-
self.logger.debug(
280-
f"Picking affiliation file with AI "
281-
f"(batch {batch_index}/{total_batches}, {len(batch)} candidates)"
282-
)
283278
candidates_with_previews = await self.format_candidates_with_previews(repo_path, batch)
284279
prompt = self.get_file_picker_prompt(
285280
repo_url,
@@ -292,9 +287,7 @@ async def pick_affiliation_file_with_ai(
292287
if result.output.file_name is not None:
293288
picked_path = result.output.file_name
294289
if picked_path not in batch:
295-
self.logger.debug(
296-
f"AI picked path not in candidate batch, skipping: {picked_path!r}"
297-
)
290+
self.logger.debug(f"AI picked invalid path, skipping: {picked_path!r}")
298291
continue
299292
full_path = os.path.join(repo_path, picked_path)
300293
if not await aiofiles.os.path.isfile(full_path):
@@ -316,25 +309,19 @@ async def discover_affiliation_file(
316309
ai_cost = 0.0
317310

318311
matches = await self.find_known_file_matches(repo_path)
319-
self.logger.debug(f"Known filename matches: {len(matches)}")
320312

321313
if len(matches) == 1:
322-
self.logger.info(f"Affiliation file: {matches[0]}")
323-
return matches[0], ai_cost
314+
only_match = matches[0]
315+
if self.is_text_file_path(only_match):
316+
self.logger.info(f"Affiliation file: {only_match}")
317+
return only_match, ai_cost
324318

325319
if len(matches) > 1:
326320
candidates = [path for path in matches if self.is_text_file_path(path)]
327321
root_files_only = False
328-
if len(matches) != len(candidates):
329-
self.logger.debug(
330-
f"Skipped {len(matches) - len(candidates)} known matches with non-text extensions"
331-
)
332322
else:
333323
candidates = await self.list_root_text_files(repo_path)
334324
root_files_only = True
335-
self.logger.debug(
336-
f"No known filename matches, checking {len(candidates)} repo root files with AI"
337-
)
338325

339326
if not candidates:
340327
return None, ai_cost
@@ -360,7 +347,6 @@ async def resolve_affiliation_file(
360347
if saved_file_path:
361348
saved_on_disk = os.path.join(repo_path, saved_file_path)
362349
if await aiofiles.os.path.isfile(saved_on_disk):
363-
self.logger.debug(f"Using saved affiliation file: {saved_file_path}")
364350
return saved_file_path, 0.0
365351
self.logger.info("Saved affiliation file is missing, looking for a new one")
366352

@@ -483,7 +469,6 @@ async def parse_affiliations(
483469
if not affiliations:
484470
return [], parse_result.cost
485471

486-
raw_count = len(affiliations)
487472
normalized = self.normalize_parsed_affiliations(affiliations)
488473

489474
if not normalized:
@@ -492,11 +477,6 @@ async def parse_affiliations(
492477
error_message="Affiliation file had rows but none were usable",
493478
)
494479

495-
if len(normalized) < raw_count:
496-
self.logger.debug(
497-
f"Dropped {raw_count - len(normalized)} rows missing email, github, or domain"
498-
)
499-
500480
return normalized, parse_result.cost
501481

502482
if parse_result.output.error == "not_found":
@@ -506,7 +486,6 @@ async def parse_affiliations(
506486
error_message="Unexpected response while parsing the affiliation file",
507487
)
508488

509-
self.logger.debug("Affiliation file is large, parsing in chunks")
510489
chunks: list[str] = []
511490
remaining = content
512491
while remaining:
@@ -542,7 +521,6 @@ async def process_chunk(chunk_index: int, chunk: str):
542521
total_cost += chunk_result.cost
543522

544523
if affiliations:
545-
raw_count = len(affiliations)
546524
normalized = self.normalize_parsed_affiliations(affiliations)
547525

548526
if not normalized:
@@ -551,11 +529,6 @@ async def process_chunk(chunk_index: int, chunk: str):
551529
error_message="Affiliation file had rows but none were usable",
552530
)
553531

554-
if len(normalized) < raw_count:
555-
self.logger.debug(
556-
f"Dropped {raw_count - len(normalized)} rows missing email, github, or domain"
557-
)
558-
559532
return normalized, total_cost
560533

561534
return [], total_cost
@@ -577,13 +550,11 @@ async def resolve_snapshot(
577550

578551
if not needs_parse:
579552
if not existing_snapshot:
580-
self.logger.debug("Using cached empty snapshot, file unchanged")
581553
return [], 0.0
582554

583555
applyable = self.normalize_parsed_affiliations(existing_snapshot)
584556

585557
if applyable:
586-
self.logger.debug("Using cached snapshot, file unchanged")
587558
return applyable, 0.0
588559

589560
self.logger.info("Cached snapshot had no usable rows, reparsing file")
@@ -598,26 +569,17 @@ async def check_if_interval_elapsed(self, registry: dict | None) -> tuple[bool,
598569
Repos with a saved file use the update interval; repos still searching use the retry interval.
599570
"""
600571
if registry is None or registry.get("last_run_at") is None:
601-
self.logger.debug("First affiliation run for this repo")
602572
return True, 0.0
603573

604574
time_since_last_run = datetime.now(timezone.utc) - registry["last_run_at"]
605575
hours_since_last_run = time_since_last_run.total_seconds() / 3600
606576

607577
if registry.get("file_path"):
608578
remaining_hours = max(0, AFFILIATION_UPDATE_INTERVAL_HOURS - hours_since_last_run)
609-
self.logger.debug(
610-
f"Last run {hours_since_last_run:.1f}h ago, "
611-
f"update interval is {AFFILIATION_UPDATE_INTERVAL_HOURS}h"
612-
)
613579
return hours_since_last_run >= AFFILIATION_UPDATE_INTERVAL_HOURS, remaining_hours
614580

615581
required_hours = AFFILIATION_RETRY_INTERVAL_DAYS * 24
616582
remaining_hours = max(0, required_hours - hours_since_last_run)
617-
self.logger.debug(
618-
f"Last run {hours_since_last_run:.1f}h ago, "
619-
f"retry interval is {AFFILIATION_RETRY_INTERVAL_DAYS}d"
620-
)
621583
return hours_since_last_run >= required_hours, remaining_hours
622584

623585
@staticmethod
@@ -627,6 +589,63 @@ def is_undated_or_open_ended(date_start, date_end) -> bool:
627589
return True
628590
return date_start is not None and date_end is None
629591

592+
@staticmethod
593+
def affiliation_identity_key(item: AffiliationInfoItem) -> tuple[str, str, str] | None:
594+
domain = item.organization.domain
595+
if not domain:
596+
return None
597+
domain = domain.lower()
598+
if item.contributor.github:
599+
return ("github", item.contributor.github.lower(), domain)
600+
if item.contributor.email:
601+
return ("email", item.contributor.email.lower(), domain)
602+
return None
603+
604+
async def exclude_parent_repo_affiliations(
605+
self,
606+
parent_repo: Repository,
607+
extracted_affiliations: list[AffiliationInfoItem] | None,
608+
) -> list[AffiliationInfoItem] | None:
609+
if not parent_repo or not extracted_affiliations:
610+
return extracted_affiliations
611+
612+
parent_registry = await get_repo_affiliation_registry(parent_repo.id)
613+
parent_repo_affiliations = (
614+
parent_registry.get("snapshot") if parent_registry else None
615+
) or []
616+
if not parent_repo_affiliations:
617+
return extracted_affiliations
618+
619+
parent_affiliation_keys = {
620+
key
621+
for item in parent_repo_affiliations
622+
if (key := self.affiliation_identity_key(item)) is not None
623+
}
624+
625+
fork_only_affiliations = [
626+
affiliation
627+
for affiliation in extracted_affiliations
628+
if (key := self.affiliation_identity_key(affiliation)) is None
629+
or key not in parent_affiliation_keys
630+
]
631+
632+
return fork_only_affiliations
633+
634+
@staticmethod
635+
def resolve_registry_status(
636+
affiliations: list[AffiliationInfoItem],
637+
registry: dict | None,
638+
file_hash: str,
639+
) -> str:
640+
if (
641+
registry
642+
and registry.get("status") == AffiliationRegistryStatus.UNUSABLE.value
643+
and registry.get("file_hash") == file_hash
644+
and not affiliations
645+
):
646+
return AffiliationRegistryStatus.UNUSABLE.value
647+
return AffiliationRegistryStatus.SUCCESS.value
648+
630649
def has_undated_affiliation_for_org(
631650
self, existing_rows: list[dict], organization_id: str
632651
) -> bool:
@@ -702,17 +721,14 @@ async def apply_affiliations(
702721

703722
unique_pairs: list[tuple[str, str]] = []
704723
seen_pairs: set[tuple[str, str]] = set()
705-
skipped_unresolved = 0
706724

707725
for member_idx, org_idx in row_identity_refs:
708726
if member_idx is None or org_idx is None:
709-
skipped_unresolved += 1
710727
continue
711728

712729
member_id = resolved_members[member_idx].get("member_id")
713730
organization_id = resolved_organizations[org_idx].get("organization_id")
714731
if not member_id or not organization_id:
715-
skipped_unresolved += 1
716732
continue
717733

718734
pair = (member_id, organization_id)
@@ -722,9 +738,7 @@ async def apply_affiliations(
722738
unique_pairs.append(pair)
723739

724740
if not unique_pairs:
725-
self.logger.debug(
726-
f"No member/org pairs resolved ({skipped_unresolved} rows could not be matched)"
727-
)
741+
self.logger.debug("No member/org pairs resolved")
728742
return
729743

730744
member_ids_to_fetch = list({member_id for member_id, _ in unique_pairs})
@@ -765,6 +779,7 @@ async def apply_affiliations(
765779
# await insert_member_organizations(mo_inserts)
766780
# await insert_member_segment_affiliations(msa_inserts)
767781

782+
# TODO: Remove this after testing
768783
self.logger.debug(
769784
f"Apply dry run: {len(mo_inserts)} MO and {len(msa_inserts)} MSA rows ready to write"
770785
)
@@ -822,13 +837,18 @@ async def process_affiliations(
822837
)
823838
ai_cost += parse_cost
824839

840+
if repository.parent_repo:
841+
affiliations = await self.exclude_parent_repo_affiliations(
842+
repository.parent_repo, affiliations
843+
)
844+
825845
await self.apply_affiliations(repository, affiliations)
826846

827847
await upsert_repo_affiliation_registry(
828848
repository.id,
829849
file_path=latest_file_path,
830850
file_hash=file_hash,
831-
status=AffiliationRegistryStatus.SUCCESS.value,
851+
status=self.resolve_registry_status(affiliations, registry, file_hash),
832852
snapshot=affiliations,
833853
)
834854

@@ -854,7 +874,11 @@ async def process_affiliations(
854874
repository.id,
855875
file_path=latest_file_path,
856876
file_hash=latest_file_hash if e.retain_file_hash else None,
857-
status=AffiliationRegistryStatus.ERROR.value,
877+
status=(
878+
AffiliationRegistryStatus.UNUSABLE.value
879+
if e.retain_file_hash
880+
else AffiliationRegistryStatus.ERROR.value
881+
),
858882
snapshot=[]
859883
if e.retain_file_hash
860884
else (registry.get("snapshot") if registry else None),
@@ -877,7 +901,7 @@ async def process_affiliations(
877901

878902
service_execution = ServiceExecution(
879903
repo_id=repository.id,
880-
operation_type=OperationType.REPO_AFFILIATION,
904+
operation_type=OperationType.AFFILIATION,
881905
status=execution_status,
882906
error_code=error_code,
883907
error_message=error_message,

0 commit comments

Comments
 (0)