uaforger 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
uaforge/__init__.py ADDED
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
File without changes
@@ -0,0 +1,97 @@
1
+ import random
2
+ from typing import List
3
+
4
+
5
+ class AliasSampler:
6
+ """
7
+ O(1) weighted random sampler using Vose's Alias Method.
8
+
9
+ Usage:
10
+ sampler = AliasSampler(weights, rng)
11
+ index = sampler.sample() # O(1) per call
12
+ """
13
+
14
+ def __init__(self, weights: List[float], rng=None):
15
+ """
16
+ Preprocess weights into alias table.
17
+
18
+ Args:
19
+ weights: List of weights (need not sum to 1, will be normalized)
20
+ rng: Random instance to use (defaults to random module)
21
+ """
22
+ self.rng = rng if rng is not None else random
23
+ n = len(weights)
24
+
25
+ if n == 0:
26
+ raise ValueError("Cannot create AliasSampler with empty weights")
27
+
28
+ self.n = n
29
+
30
+ # Normalize weights
31
+ total = sum(weights)
32
+ if total <= 0:
33
+ raise ValueError("Sum of weights must be positive")
34
+
35
+ # probabilities normalized to sum to n (for the algorithm)
36
+ prob = [w * n / total for w in weights]
37
+
38
+ # Alias tables
39
+ self.prob = [0.0] * n
40
+ self.alias = [0] * n
41
+
42
+ # Partition into small and large
43
+ small = []
44
+ large = []
45
+
46
+ for i, p in enumerate(prob):
47
+ if p < 1.0:
48
+ small.append(i)
49
+ else:
50
+ large.append(i)
51
+
52
+ # Build alias table
53
+ while small and large:
54
+ l = small.pop()
55
+ g = large.pop()
56
+
57
+ self.prob[l] = prob[l]
58
+ self.alias[l] = g
59
+
60
+ prob[g] = prob[g] + prob[l] - 1.0
61
+
62
+ if prob[g] < 1.0:
63
+ small.append(g)
64
+ else:
65
+ large.append(g)
66
+
67
+ # Remaining items (due to floating point, both could have leftovers)
68
+ while large:
69
+ g = large.pop()
70
+ self.prob[g] = 1.0
71
+
72
+ while small:
73
+ l = small.pop()
74
+ self.prob[l] = 1.0
75
+
76
+ def sample(self, rand: random.Random = None) -> int:
77
+ """
78
+ Sample an index in O(1) time.
79
+
80
+ Args:
81
+ rand: Optional random instance to use instead of self.rng
82
+
83
+ Returns:
84
+ Sampled index
85
+ """
86
+ rng = rand if rand is not None else self.rng
87
+ # Generate fair die roll
88
+ i = rng.randrange(self.n)
89
+ # Flip biased coin
90
+ if rng.random() < self.prob[i]:
91
+ return i
92
+ else:
93
+ return self.alias[i]
94
+
95
+ def sample_n(self, n: int) -> List[int]:
96
+ """Sample n indices efficiently."""
97
+ return [self.sample() for _ in range(n)]
@@ -0,0 +1,232 @@
1
+ import random
2
+ from typing import List, Tuple, Optional, TYPE_CHECKING
3
+ from ..models.enums import BrowserFamily, DeviceType
4
+
5
+ if TYPE_CHECKING:
6
+ from ..data.loader import DataLoader
7
+
8
+
9
+ class ClientHintsGenerator:
10
+ """
11
+ Generates the modern 'Sec-CH-UA' headers.
12
+ """
13
+
14
+ # General GREASE token
15
+ GREASE_BRAND = list("Not A Brand")
16
+ PUNCT = ";:()_ "
17
+ _brand_base_cache = {}
18
+
19
+ @staticmethod
20
+ def _format_brand_list(brands: List[Tuple[str, str]]) -> str:
21
+ parts = []
22
+ for brand, version in brands:
23
+ parts.append(f'"{brand}";v="{version}"')
24
+ return ", ".join(parts)
25
+
26
+ @classmethod
27
+ def _get_brand_tuples(cls, family: BrowserFamily, version: str, rand=None) -> List[Tuple[str, str]]:
28
+ """Randomized brand tuples. Accepts an optional RNG instance for faster and thread-safe sampling."""
29
+ if rand is None:
30
+ rand = random
31
+
32
+ brands: List[Tuple[str, str]] = []
33
+ name = cls.GREASE_BRAND.copy()
34
+ space_idxs = [i for i, c in enumerate(name) if c == " "]
35
+
36
+ if len(space_idxs) >= 2:
37
+ sep1, sep2 = rand.choices(cls.PUNCT, k=2)
38
+ name[space_idxs[0]] = sep1
39
+ name[space_idxs[1]] = sep2
40
+
41
+ grease_name = "".join(name)
42
+ grease_version = rand.choice(("99", "8", "24"))
43
+ brands.append((grease_name, grease_version))
44
+
45
+ if family == BrowserFamily.CHROME:
46
+ brands.append(("Chromium", version))
47
+ brands.append(("Google Chrome", version))
48
+ elif family == BrowserFamily.EDGE:
49
+ brands.append(("Chromium", version))
50
+ brands.append(("Microsoft Edge", version))
51
+ elif family == BrowserFamily.OPERA:
52
+ brands.append(("Chromium", version))
53
+ brands.append(("Opera", version))
54
+ else:
55
+ brands.append(("Chromium", version))
56
+
57
+ # Shuffle to randomize order
58
+ rand.shuffle(brands)
59
+
60
+ return brands
61
+
62
+ @classmethod
63
+ def generate_brands(cls, family: BrowserFamily, major_version: str, rand=None) -> str:
64
+ """
65
+ Constructs the Sec-CH-UA header value (Major versions only).
66
+ Returns EMPTY STRING for Safari/Firefox.
67
+
68
+ For Edge and Opera, uses appropriate Chromium major version:
69
+ - Edge: Same major version as Edge
70
+ - Opera: Opera major version + 16
71
+ """
72
+ if family in (BrowserFamily.FIREFOX, BrowserFamily.SAFARI):
73
+ return ""
74
+
75
+ # Get Chromium major version for Edge and Opera
76
+ chromium_major = None
77
+ if family == BrowserFamily.EDGE:
78
+ chromium_major = major_version # Edge uses same major as Chromium
79
+ elif family == BrowserFamily.OPERA:
80
+ try:
81
+ chromium_major = str(int(major_version) + 16)
82
+ except ValueError:
83
+ chromium_major = major_version
84
+
85
+ # Build the brand list
86
+ parts = []
87
+ parts.append('"Not A Brand";v="99"')
88
+
89
+ if family == BrowserFamily.CHROME:
90
+ parts.append(f'"Chromium";v="{major_version}"')
91
+ parts.append(f'"Google Chrome";v="{major_version}"')
92
+ elif family == BrowserFamily.EDGE:
93
+ parts.append(f'"Chromium";v="{chromium_major}"')
94
+ parts.append(f'"Microsoft Edge";v="{major_version}"')
95
+ elif family == BrowserFamily.OPERA:
96
+ parts.append(f'"Chromium";v="{chromium_major}"')
97
+ parts.append(f'"Opera";v="{major_version}"')
98
+ else:
99
+ parts.append(f'"Chromium";v="{major_version}"')
100
+
101
+ return ", ".join(parts)
102
+
103
+ @classmethod
104
+ def get_major_chromium_full_version(cls, family: BrowserFamily, full_version: str, rand=None, loader: Optional['DataLoader'] = None) -> Optional[int]:
105
+ chromium_version = None
106
+ if loader:
107
+ if family == BrowserFamily.EDGE:
108
+ try:
109
+ major = full_version.split('.')[0]
110
+ chromium_version = loader.get_chromium_version_for_edge(major)
111
+ except Exception:
112
+ pass
113
+ elif family == BrowserFamily.OPERA:
114
+ try:
115
+ major = full_version.split('.')[0]
116
+ chromium_version = loader.get_chromium_version_for_opera(major)
117
+ except Exception:
118
+ pass
119
+ if chromium_version:
120
+ return int(chromium_version.split('.')[0])
121
+ elif family == BrowserFamily.CHROME:
122
+ return int(full_version.split('.')[0])
123
+ return -1
124
+
125
+
126
+ @classmethod
127
+ def generate_full_version_list(cls, family: BrowserFamily, full_version: str, rand=None, loader: Optional['DataLoader'] = None) -> str:
128
+ """
129
+ Constructs the Sec-CH-UA-Full-Version-List header.
130
+ Returns EMPTY STRING for Safari/Firefox.
131
+
132
+ For Edge and Opera, uses appropriate Chromium version:
133
+ - Edge: Same major version as Edge
134
+ - Opera: Opera major version + 16
135
+ """
136
+ if family in (BrowserFamily.FIREFOX, BrowserFamily.SAFARI):
137
+ return ""
138
+
139
+ # Get Chromium version for Edge and Opera
140
+ chromium_version = None
141
+ if loader:
142
+ if family == BrowserFamily.EDGE:
143
+ try:
144
+ major = full_version.split('.')[0]
145
+ chromium_version = loader.get_chromium_version_for_edge(major)
146
+ except Exception:
147
+ pass
148
+ elif family == BrowserFamily.OPERA:
149
+ try:
150
+ major = full_version.split('.')[0]
151
+ chromium_version = loader.get_chromium_version_for_opera(major)
152
+ except Exception:
153
+ pass
154
+
155
+ # Build the version list
156
+ parts = []
157
+ parts.append('"Not A Brand";v="99.0.0.0"')
158
+
159
+ if family == BrowserFamily.CHROME:
160
+ parts.append(f'"Chromium";v="{full_version}"')
161
+ parts.append(f'"Google Chrome";v="{full_version}"')
162
+ elif family == BrowserFamily.EDGE:
163
+ if chromium_version:
164
+ parts.append(f'"Chromium";v="{chromium_version}"')
165
+ else:
166
+ parts.append(f'"Chromium";v="{full_version}"')
167
+ parts.append(f'"Microsoft Edge";v="{full_version}"')
168
+ elif family == BrowserFamily.OPERA:
169
+ if chromium_version:
170
+ parts.append(f'"Chromium";v="{chromium_version}"')
171
+ else:
172
+ # Fallback: calculate Chromium version
173
+ major = int(full_version.split('.')[0])
174
+ chromium_major = major + 16
175
+ parts.append(f'"Chromium";v="{chromium_major}.0.0.0"')
176
+ parts.append(f'"Opera";v="{full_version}"')
177
+ else:
178
+ parts.append(f'"Chromium";v="{full_version}"')
179
+
180
+ return ", ".join(parts)
181
+
182
+ @staticmethod
183
+ def get_mobile_token(is_mobile: bool) -> str:
184
+ return "?1" if is_mobile else "?0"
185
+
186
+ @staticmethod
187
+ def get_platform_token(platform: str) -> str:
188
+ return platform
189
+
190
+ @classmethod
191
+ def generate_full_version(cls, family: BrowserFamily, full_version: str) -> str:
192
+ """
193
+ Constructs the Sec-CH-UA-Full-Version header.
194
+ Returns EMPTY STRING for Safari/Firefox.
195
+ """
196
+ if family in (BrowserFamily.FIREFOX, BrowserFamily.SAFARI):
197
+ return ""
198
+
199
+ # Return the full version as-is (e.g., "142.0.7444.175")
200
+ return full_version
201
+
202
+ @classmethod
203
+ def generate_form_factors(cls, device_type: DeviceType, rand=None) -> str:
204
+ """
205
+ Constructs the Sec-CH-UA-Form-Factors header.
206
+ """
207
+ if rand is None:
208
+ rand = random
209
+
210
+ if device_type == DeviceType.DESKTOP:
211
+ return "Desktop"
212
+ elif device_type == DeviceType.TABLET:
213
+ return "Tablet"
214
+ else: # MOBILE
215
+ return "Mobile"
216
+
217
+ @staticmethod
218
+ def get_wow64_token(is_wow64: bool) -> str:
219
+ """
220
+ Constructs the Sec-CH-UA-WoW64 header.
221
+ """
222
+ return "?1" if is_wow64 else "?0"
223
+
224
+ @staticmethod
225
+ def get_prefers_color_scheme(rand=None) -> str:
226
+ """
227
+ Constructs the Sec-CH-Prefers-Color-Scheme header.
228
+ """
229
+ if rand is None:
230
+ rand = random
231
+ # Randomly choose between light and dark themes
232
+ return rand.choice(["light", "dark"])