mirror of https://github.com/Chizi123/Arch-autobuild-repo.git

Joel Grunbaum
2 days ago dc96e98618fe4739210d10ee546b105e36433d02
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
"""Async AUR RPC API client with caching and retry logic."""
 
import asyncio
from dataclasses import dataclass, field
from datetime import datetime, timedelta
from enum import Enum
from typing import Any
 
import aiohttp
 
from archbuild.logging import get_logger
 
logger = get_logger("aur")
 
AUR_RPC_URL = "https://aur.archlinux.org/rpc"
AUR_PACKAGE_URL = "https://aur.archlinux.org/packages"
AUR_GIT_URL = "https://aur.archlinux.org"
 
 
class PackageType(Enum):
    """Package type from AUR."""
 
    NORMAL = "normal"
    SPLIT = "split"
 
 
@dataclass
class Package:
    """AUR package metadata."""
 
    name: str
    version: str
    description: str
    url: str | None
    maintainer: str | None
    votes: int
    popularity: float
    out_of_date: datetime | None
    first_submitted: datetime
    last_modified: datetime
    depends: list[str] = field(default_factory=list)
    makedepends: list[str] = field(default_factory=list)
    checkdepends: list[str] = field(default_factory=list)
    optdepends: list[str] = field(default_factory=list)
    provides: list[str] = field(default_factory=list)
    conflicts: list[str] = field(default_factory=list)
    replaces: list[str] = field(default_factory=list)
    license: list[str] = field(default_factory=list)
    keywords: list[str] = field(default_factory=list)
 
    @property
    def git_url(self) -> str:
        """Get the git clone URL for this package."""
        return f"{AUR_GIT_URL}/{self.name}.git"
 
    @property
    def aur_url(self) -> str:
        """Get the AUR web page URL for this package."""
        return f"{AUR_PACKAGE_URL}/{self.name}"
 
    @classmethod
    def from_rpc(cls, data: dict[str, Any]) -> "Package":
        """Create Package from AUR RPC response data.
 
        Args:
            data: Package data from AUR RPC API
 
        Returns:
            Package instance
        """
        return cls(
            name=data["Name"],
            version=data["Version"],
            description=data.get("Description", ""),
            url=data.get("URL"),
            maintainer=data.get("Maintainer"),
            votes=data.get("NumVotes", 0),
            popularity=data.get("Popularity", 0.0),
            out_of_date=(
                datetime.fromtimestamp(data["OutOfDate"]) if data.get("OutOfDate") else None
            ),
            first_submitted=datetime.fromtimestamp(data["FirstSubmitted"]),
            last_modified=datetime.fromtimestamp(data["LastModified"]),
            depends=data.get("Depends", []),
            makedepends=data.get("MakeDepends", []),
            checkdepends=data.get("CheckDepends", []),
            optdepends=data.get("OptDepends", []),
            provides=data.get("Provides", []),
            conflicts=data.get("Conflicts", []),
            replaces=data.get("Replaces", []),
            license=data.get("License", []),
            keywords=data.get("Keywords", []),
        )
 
 
@dataclass
class CacheEntry:
    """Cache entry with TTL."""
 
    data: Package
    expires: datetime
 
 
class AURClient:
    """Async client for AUR RPC API with caching and retry."""
 
    def __init__(
        self,
        cache_ttl: int = 300,
        max_retries: int = 3,
        retry_delay: float = 1.0,
        batch_size: int = 100,
    ):
        """Initialize AUR client.
 
        Args:
            cache_ttl: Cache time-to-live in seconds
            max_retries: Maximum number of retry attempts
            retry_delay: Base delay between retries (exponential backoff)
            batch_size: Maximum packages per batch request
        """
        self.cache_ttl = cache_ttl
        self.max_retries = max_retries
        self.retry_delay = retry_delay
        self.batch_size = batch_size
        self._cache: dict[str, CacheEntry] = {}
        self._session: aiohttp.ClientSession | None = None
 
    async def __aenter__(self) -> "AURClient":
        """Async context manager entry."""
        self._session = aiohttp.ClientSession()
        return self
 
    async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
        """Async context manager exit."""
        if self._session:
            await self._session.close()
            self._session = None
 
    def _get_cached(self, name: str) -> Package | None:
        """Get package from cache if not expired.
 
        Args:
            name: Package name
 
        Returns:
            Cached package or None if not cached/expired
        """
        entry = self._cache.get(name)
        if entry and entry.expires > datetime.now():
            return entry.data
        return None
 
    def _set_cached(self, package: Package) -> None:
        """Store package in cache.
 
        Args:
            package: Package to cache
        """
        self._cache[package.name] = CacheEntry(
            data=package,
            expires=datetime.now() + timedelta(seconds=self.cache_ttl),
        )
 
    async def _request(
        self,
        params: list[tuple[str, Any]] | dict[str, Any],
    ) -> dict[str, Any]:
        """Make request to AUR RPC API with retry logic.
 
        Args:
            params: Query parameters (as list of tuples for repeated keys, or dict)
 
        Returns:
            JSON response data
 
        Raises:
            aiohttp.ClientError: If request fails after all retries
        """
        if not self._session:
            raise RuntimeError("AURClient must be used as async context manager")
 
        last_error: Exception | None = None
 
        for attempt in range(self.max_retries):
            try:
                async with self._session.get(AUR_RPC_URL, params=params) as response:
                    response.raise_for_status()
                    data = await response.json()
                    if data.get("type") == "error":
                        raise ValueError(f"AUR API error: {data.get('error')}")
                    return data
            except (aiohttp.ClientError, asyncio.TimeoutError) as e:
                last_error = e
                if attempt < self.max_retries - 1:
                    delay = self.retry_delay * (2**attempt)
                    logger.warning(
                        f"AUR request failed (attempt {attempt + 1}/{self.max_retries}), "
                        f"retrying in {delay}s: {e}"
                    )
                    await asyncio.sleep(delay)
 
        raise last_error or RuntimeError("Request failed")
 
    async def get_package(self, name: str) -> Package | None:
        """Get a single package by name.
 
        Args:
            name: Package name
 
        Returns:
            Package if found, None otherwise
        """
        # Check cache first
        cached = self._get_cached(name)
        if cached:
            logger.debug(f"Cache hit for package: {name}")
            return cached
 
        packages = await self.get_packages([name])
        return packages[0] if packages else None
 
    async def get_packages(self, names: list[str]) -> list[Package]:
        """Get multiple packages by name using batch queries.
 
        Args:
            names: List of package names
 
        Returns:
            List of found packages (may be fewer than requested)
        """
        # Separate cached and uncached packages
        result: list[Package] = []
        uncached: list[str] = []
 
        for name in names:
            cached = self._get_cached(name)
            if cached:
                result.append(cached)
            else:
                uncached.append(name)
 
        if not uncached:
            return result
 
        # Batch request uncached packages
        for i in range(0, len(uncached), self.batch_size):
            batch = uncached[i : i + self.batch_size]
            
            # Build params as list of tuples for repeated arg[] keys
            params: list[tuple[str, Any]] = [("v", 5), ("type", "info")]
            for name in batch:
                params.append(("arg[]", name))
 
            data = await self._request(params)
 
            for pkg_data in data.get("results", []):
                package = Package.from_rpc(pkg_data)
                self._set_cached(package)
                result.append(package)
 
        return result
 
    async def search(self, query: str, by: str = "name-desc") -> list[Package]:
        """Search AUR packages.
 
        Args:
            query: Search query string
            by: Search field (name, name-desc, maintainer, depends, makedepends, optdepends, checkdepends)
 
        Returns:
            List of matching packages
        """
        params = {"v": 5, "type": "search", "by": by, "arg": query}
        data = await self._request(params)
 
        packages = []
        for pkg_data in data.get("results", []):
            package = Package.from_rpc(pkg_data)
            self._set_cached(package)
            packages.append(package)
 
        return packages
 
    async def is_available(self, name: str) -> bool:
        """Check if a package exists in the AUR.
 
        Args:
            name: Package name
 
        Returns:
            True if package exists
        """
        package = await self.get_package(name)
        return package is not None
 
    def clear_cache(self) -> None:
        """Clear the package cache."""
        self._cache.clear()