Skip to content

Commit 9dfde3d

Browse files
committed
Merge remote-tracking branch 'upstream/test' into fix/mirror-outage-cache-fallback-mixed-source
# Conflicts: # tests/validator/test_validator_cache_fallback.py
2 parents 1895dec + 34ffeb8 commit 9dfde3d

14 files changed

Lines changed: 564 additions & 81 deletions

File tree

‎gittensor/classes.py‎

Lines changed: 47 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1+
import copy
12
import re
2-
from copy import deepcopy
33
from dataclasses import dataclass, field
44
from datetime import datetime, timezone
55
from enum import Enum
@@ -80,6 +80,9 @@ def is_test_file(self) -> bool:
8080
test_dir_patterns = [
8181
r'(^|/)tests?/',
8282
r'(^|/)__tests?__/',
83+
r'(^|/)androidtest[a-z]*/',
84+
r'(^|/)integrationtest/',
85+
r'(^|/)spec/',
8386
]
8487
if any(re.search(pattern, filename_lower) for pattern in test_dir_patterns):
8588
return True
@@ -89,6 +92,7 @@ def is_test_file(self) -> bool:
8992
r'^spec_',
9093
r'_test\.[^.]+$',
9194
r'_tests\.[^.]+$',
95+
r'_spec\.[^.]+$',
9296
r'\.test\.[^.]+$',
9397
r'\.tests\.[^.]+$',
9498
r'\.spec\.[^.]+$',
@@ -659,7 +663,7 @@ def store(self, evaluation: 'MinerEvaluation') -> None:
659663
if not evaluation.hotkey or not evaluation.github_id or evaluation.github_id == '0':
660664
return
661665

662-
cached_eval = self.create_lightweight_copy(evaluation)
666+
cached_eval = self._build_cache_entry(evaluation)
663667

664668
self._cache[evaluation.uid] = CachedEvaluation(
665669
hotkey=evaluation.hotkey,
@@ -694,17 +698,44 @@ def get(self, uid: int, hotkey: str, github_id: str) -> Optional['MinerEvaluatio
694698

695699
bt.logging.debug(f'Cache hit for UID {uid} (cached at {cached.cached_at.isoformat()})')
696700

697-
return deepcopy(cached.evaluation)
698-
699-
def create_lightweight_copy(self, evaluation: 'MinerEvaluation') -> 'MinerEvaluation':
700-
"""Create a memory-efficient copy, stripping file patches."""
701-
light_eval = deepcopy(evaluation)
702-
703-
for pr in light_eval.merged_pull_requests + light_eval.open_pull_requests + light_eval.closed_pull_requests:
704-
if pr.file_changes:
705-
for fc in pr.file_changes:
706-
fc.patch = None
707-
708-
light_eval.github_pat = None
709-
710-
return light_eval
701+
return self._isolate_for_downstream(cached.evaluation)
702+
703+
@staticmethod
704+
def _build_cache_entry(evaluation: 'MinerEvaluation') -> 'MinerEvaluation':
705+
# Cached evaluations feed only the GitHub-fetch-failure fallback path
706+
# (issue_competitions + issue discovery scoring), which never reads
707+
# file_changes. Drop them at store time to save memory and avoid
708+
# copying thousands of FileChange objects per miner.
709+
cached = copy.copy(evaluation)
710+
cached.github_pat = None
711+
cached.unique_repos_contributed_to = set(evaluation.unique_repos_contributed_to)
712+
cached.merged_pull_requests = [_pr_for_cache(pr) for pr in evaluation.merged_pull_requests]
713+
cached.open_pull_requests = [_pr_for_cache(pr) for pr in evaluation.open_pull_requests]
714+
cached.closed_pull_requests = [_pr_for_cache(pr) for pr in evaluation.closed_pull_requests]
715+
return cached
716+
717+
@staticmethod
718+
def _isolate_for_downstream(cached_eval: 'MinerEvaluation') -> 'MinerEvaluation':
719+
# Downstream scoring mutates top-level scalar fields on MinerEvaluation
720+
# and discovery_* fields on Issue. Everything else (PR metadata) is
721+
# read-only on the cache-fallback path, so we can share it.
722+
copy_eval = copy.copy(cached_eval)
723+
copy_eval.unique_repos_contributed_to = set(cached_eval.unique_repos_contributed_to)
724+
copy_eval.merged_pull_requests = [_pr_with_fresh_issues(pr) for pr in cached_eval.merged_pull_requests]
725+
copy_eval.open_pull_requests = [_pr_with_fresh_issues(pr) for pr in cached_eval.open_pull_requests]
726+
copy_eval.closed_pull_requests = [_pr_with_fresh_issues(pr) for pr in cached_eval.closed_pull_requests]
727+
return copy_eval
728+
729+
730+
def _pr_for_cache(pr: 'PullRequest') -> 'PullRequest':
731+
pr_copy = copy.copy(pr)
732+
pr_copy.file_changes = None
733+
pr_copy.issues = [copy.copy(issue) for issue in pr.issues] if pr.issues else None
734+
return pr_copy
735+
736+
737+
def _pr_with_fresh_issues(pr: 'PullRequest') -> 'PullRequest':
738+
pr_copy = copy.copy(pr)
739+
if pr.issues is not None:
740+
pr_copy.issues = [copy.copy(issue) for issue in pr.issues]
741+
return pr_copy

‎gittensor/cli/issue_commands/helpers.py‎

Lines changed: 8 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -696,31 +696,6 @@ def _resolve_contract_and_network(
696696
# ============================================================================
697697

698698

699-
def _get_contract_child_storage_key(substrate, contract_addr: str, verbose: bool = False) -> Optional[str]:
700-
"""
701-
Get the child storage key for a contract's trie.
702-
703-
Args:
704-
substrate: SubstrateInterface instance
705-
contract_addr: Contract address
706-
verbose: If True, print debug output
707-
708-
Returns:
709-
Hex-encoded child storage key or None if contract doesn't exist
710-
"""
711-
try:
712-
child_key = get_contract_child_storage_key(substrate, contract_addr)
713-
if not child_key:
714-
if verbose:
715-
console.print(f'[dim]Debug: Contract not found at {contract_addr}[/dim]')
716-
return None
717-
return child_key
718-
except Exception as e:
719-
if verbose:
720-
console.print(f'[dim]Debug: Contract info query failed: {e}[/dim]')
721-
return None
722-
723-
724699
def _read_contract_packed_storage(substrate, contract_addr: str, verbose: bool = False) -> Optional[Dict[str, Any]]:
725700
"""
726701
Read the packed root storage from a contract using childstate RPC
@@ -785,10 +760,16 @@ def _read_issues_from_child_storage(substrate, contract_addr: str, verbose: bool
785760
Returns:
786761
List of issue dictionaries
787762
"""
788-
child_key = _get_contract_child_storage_key(substrate, contract_addr, verbose)
763+
try:
764+
child_key = get_contract_child_storage_key(substrate, contract_addr)
765+
except Exception as e:
766+
if verbose:
767+
console.print(f'[dim]Debug: Contract info query failed: {e}[/dim]')
768+
child_key = None
769+
789770
if not child_key:
790771
if verbose:
791-
console.print('[dim]Debug: Cannot read issues - no child storage key[/dim]')
772+
console.print(f'[dim]Debug: Cannot read issues - no child storage key for {contract_addr}[/dim]')
792773
return []
793774

794775
# First, read packed storage to get next_issue_id

‎gittensor/cli/main.py‎

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -92,15 +92,21 @@ def show_config():
9292
console.print(f'[red]Error reading config: {e}[/red]')
9393

9494

95+
CONFIG_KEYS = ('wallet', 'hotkey', 'network', 'contract_address', 'ws_endpoint')
96+
97+
9598
@config_group.command('set')
96-
@click.argument('key', type=str)
99+
@click.argument('key', type=click.Choice(CONFIG_KEYS, case_sensitive=False))
97100
@click.argument('value', type=str)
98101
def config_set(key: str, value: str):
99102
"""Set a configuration value.
100103
101-
[dim]Use this command to override values stored in `~/.gittensor/config.json`.[/dim]
104+
[dim]Use this command to override values stored in `~/.gittensor/config.json`.
105+
KEY must be one of the recognised settings — unknown keys are rejected so a
106+
typo (for example `wallet_name`) cannot silently write a dead entry that
107+
downstream commands will ignore.[/dim]
102108
103-
[dim]Common keys:
109+
[dim]Recognised keys:
104110
wallet Wallet name
105111
hotkey Hotkey name
106112
contract_address Contract address
@@ -114,6 +120,7 @@ def config_set(key: str, value: str):
114120
$ gitt config set network local
115121
[/dim]
116122
"""
123+
key = key.lower()
117124
# Ensure config directory exists
118125
GITTENSOR_DIR.mkdir(parents=True, exist_ok=True)
119126

‎gittensor/utils/github_api_tools.py‎

Lines changed: 61 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,11 @@
33
import fnmatch
44
import os
55
import time
6+
from contextlib import contextmanager
67
from dataclasses import dataclass
78
from datetime import datetime, timedelta, timezone
89
from math import ceil
9-
from typing import TYPE_CHECKING, Any, Dict, List, Optional
10+
from typing import TYPE_CHECKING, Any, Dict, Iterator, List, Optional
1011

1112
from gittensor.utils.utils import parse_repo_name
1213

@@ -174,7 +175,51 @@ def make_headers(token: str) -> Dict[str, str]:
174175

175176
def make_graphql_headers(token: str) -> Dict[str, str]:
176177
"""Build GitHub GraphQL headers for a PAT."""
177-
return {'Authorization': f'Bearer {token}', 'Content-Type': 'application/json'}
178+
return {
179+
'Authorization': f'Bearer {token}',
180+
'Content-Type': 'application/json',
181+
'Accept': 'application/json',
182+
}
183+
184+
185+
def make_anonymous_headers() -> Dict[str, str]:
186+
"""Build GitHub HTTP headers for unauthenticated calls."""
187+
return {'Accept': 'application/vnd.github.v3+json', 'User-Agent': 'gittensor-cli'}
188+
189+
190+
_session_cache: Optional[Dict[str, requests.Session]] = None
191+
192+
193+
@contextmanager
194+
def session_scope() -> Iterator[None]:
195+
"""Share requests.Session per PAT within the scope; close all on exit. Not re-entrant."""
196+
global _session_cache
197+
if _session_cache is not None:
198+
raise RuntimeError('session_scope is not re-entrant')
199+
_session_cache = {}
200+
try:
201+
yield
202+
finally:
203+
cache = _session_cache
204+
_session_cache = None
205+
for s in cache.values():
206+
s.close()
207+
208+
209+
def _build_session(token: str) -> requests.Session:
210+
session = requests.Session()
211+
session.headers.update(make_headers(token) if token else make_anonymous_headers())
212+
return session
213+
214+
215+
def get_session(token: str) -> requests.Session:
216+
"""Return a requests.Session for the given PAT, reusing one within a session_scope() and allocating fresh otherwise."""
217+
if _session_cache is None:
218+
return _build_session(token)
219+
key = token or ''
220+
if key not in _session_cache:
221+
_session_cache[key] = _build_session(token)
222+
return _session_cache[key]
178223

179224

180225
def get_github_id(token: str) -> Optional[str]:
@@ -189,12 +234,12 @@ def get_github_id(token: str) -> Optional[str]:
189234
if not token:
190235
return None
191236

192-
headers = make_headers(token)
237+
session = get_session(token)
193238

194239
# Retry logic for timeout issues
195240
for attempt in range(6):
196241
try:
197-
response = requests.get(f'{BASE_GITHUB_API_URL}/user', headers=headers, timeout=GITHUB_HTTP_TIMEOUT_SECONDS)
242+
response = session.get(f'{BASE_GITHUB_API_URL}/user', timeout=GITHUB_HTTP_TIMEOUT_SECONDS)
198243
if response.status_code == 200:
199244
try:
200245
user_data: Dict[str, Any] = response.json()
@@ -235,14 +280,13 @@ def get_merge_base_sha(repository: str, base_sha: str, head_sha: str, token: str
235280
Returns:
236281
Merge-base commit SHA, or None if the request fails
237282
"""
238-
headers = make_headers(token)
283+
session = get_session(token)
239284
max_attempts = 3
240285

241286
for attempt in range(max_attempts):
242287
try:
243-
response = requests.get(
288+
response = session.get(
244289
f'{BASE_GITHUB_API_URL}/repos/{repository}/compare/{base_sha}...{head_sha}',
245-
headers=headers,
246290
timeout=15,
247291
)
248292

@@ -293,7 +337,7 @@ def get_pull_request_file_changes(repository: str, pr_number: int, token: str) -
293337
"""
294338
max_attempts = 3
295339
per_page = 100
296-
headers = make_headers(token)
340+
session = get_session(token)
297341

298342
all_file_diffs: list = []
299343
page = 1
@@ -302,9 +346,8 @@ def get_pull_request_file_changes(repository: str, pr_number: int, token: str) -
302346

303347
while attempt < max_attempts:
304348
try:
305-
response = requests.get(
349+
response = session.get(
306350
f'{BASE_GITHUB_API_URL}/repos/{repository}/pulls/{pr_number}/files',
307-
headers=headers,
308351
params={'per_page': per_page, 'page': page},
309352
timeout=15,
310353
)
@@ -485,20 +528,15 @@ def _search_issue_referencing_prs_rest(
485528
if issue_number < 1:
486529
return []
487530

488-
if token:
489-
headers = make_headers(token)
490-
else:
491-
headers = {'Accept': 'application/vnd.github.v3+json'}
492-
headers.setdefault('User-Agent', 'gittensor-cli')
531+
session = get_session(token or '')
493532

494533
state_clause = f' state:{state}' if state != 'all' else ''
495534
max_attempts = 3
496535
for attempt in range(max_attempts):
497536
try:
498-
resp = requests.get(
537+
resp = session.get(
499538
f'{BASE_GITHUB_API_URL}/search/issues',
500539
params={'q': f'repo:{repo} type:pr{state_clause} {issue_number} in:title,body', 'per_page': '50'},
501-
headers=headers,
502540
timeout=10,
503541
)
504542
resp.raise_for_status()
@@ -593,11 +631,12 @@ def execute_graphql_query(
593631
Returns:
594632
Parsed JSON response data, or None if all attempts failed
595633
"""
634+
session = get_session(token)
596635
headers = make_graphql_headers(token)
597636

598637
for attempt in range(max_attempts):
599638
try:
600-
response = requests.post(
639+
response = session.post(
601640
f'{BASE_GITHUB_API_URL}/graphql',
602641
headers=headers,
603642
json={'query': query, 'variables': variables},
@@ -670,6 +709,7 @@ def get_github_graphql_query(
670709
"""
671710

672711
max_attempts = 8
712+
session = get_session(token)
673713
headers = make_graphql_headers(token)
674714
limit = page_size if page_size is not None else min(100, max_prs - merged_pr_count)
675715

@@ -681,7 +721,7 @@ def get_github_graphql_query(
681721
'maxChangesRequestedReviews': _MAX_CHANGES_REQUESTED_REVIEWS,
682722
}
683723
try:
684-
response = requests.post(
724+
response = session.post(
685725
f'{BASE_GITHUB_API_URL}/graphql',
686726
headers=headers,
687727
json={'query': QUERY, 'variables': variables},
@@ -1068,12 +1108,11 @@ def check_github_issue_closed(repo: str, issue_number: int, token: str) -> Optio
10681108
Returns:
10691109
Dict with 'is_closed', 'solver_github_id', 'pr_number', 'solver_lookup_failed' or None on error
10701110
"""
1071-
headers = make_headers(token)
1111+
session = get_session(token)
10721112

10731113
try:
1074-
response = requests.get(
1114+
response = session.get(
10751115
f'{BASE_GITHUB_API_URL}/repos/{repo}/issues/{issue_number}',
1076-
headers=headers,
10771116
timeout=15,
10781117
)
10791118

‎gittensor/utils/mirror/client.py‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,15 @@ def __init__(
4343
self.max_attempts = max_attempts
4444
self.session = session or requests.Session()
4545

46+
def close(self) -> None:
47+
self.session.close()
48+
49+
def __enter__(self) -> 'MirrorClient':
50+
return self
51+
52+
def __exit__(self, exc_type, exc, tb) -> None:
53+
self.close()
54+
4655
def get_miner_pulls(
4756
self,
4857
github_id: str,

‎gittensor/validator/issue_competitions/forward.py‎

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,10 @@
99

1010
from gittensor.classes import MinerEvaluation
1111
from gittensor.utils.github_api_tools import check_github_issue_closed
12+
from gittensor.utils.utils import get_contract_address
1213
from gittensor.validator.issue_competitions.contract_client import IssueCompetitionContractClient, IssueStatus
1314
from gittensor.validator.utils.config import GITTENSOR_VALIDATOR_PAT
14-
from gittensor.validator.utils.issue_competitions import (
15-
get_contract_address,
16-
get_miner_coldkey,
17-
)
15+
from gittensor.validator.utils.issue_competitions import get_miner_coldkey
1816

1917
if TYPE_CHECKING:
2018
from neurons.base.validator import BaseValidatorNeuron

0 commit comments

Comments
 (0)