Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Efficient version ranges #14

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ print(ua) # Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_3) AppleWebKit/604.1.38

```python
device = ('desktop', 'mobile')
platform = ('windows', 'macos', 'ios', 'linux', 'android')
platform = ('windows', 'macos', 'ios', 'linux', 'android','android_nexus','android_pixel','android_samsung')
browser = ('chrome', 'edge', 'firefox', 'safari')
```
_All parameters are optional and multiple types can be specified using a tuple._
Expand Down
34 changes: 33 additions & 1 deletion src/ua_generator/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,45 @@
Copyright: 2022-2024 Ekin Karadeniz (github.com/iamdual)
License: Apache License 2.0
"""
#ua-generator/src/__init__.py
import typing

from . import user_agent, options as _options
from src.ua_generator.data import VERSION_SUPPORTED_MODULES
from src.ua_generator.data.browsers import chrome,firefox,edge,safari
from src.ua_generator.data.platforms import ios, macos, windows,linux
from src.ua_generator.data.platforms.android import android_nexus,android_samsung,android_pixel
from src.ua_generator.exceptions import InvalidArgumentError
versions_idx_map = {"chrome":{}, 'edge':{},'safari':{},'firefox':{},'ios':{},'windows':{},'macos':{},'linux':{},'android_samsung':{},'android_nexus':{},'android_pixel':{}}
"""
Initialize an index map containing a mapping of version to index in the original
respective versions array. Only occurs once upon the first generate using
options as a parameter. Verified with print statements.
"""
def initialize_idx_map(option):
global versions_idx_map
module = globals()[option]
for idx,val in enumerate(module.versions):
if(val.major not in versions_idx_map[option]):
versions_idx_map[option][val.major] = idx
module.versions_idx_map = versions_idx_map[option]

def get_versions(option:str):
if option in VERSION_SUPPORTED_MODULES:
module = globals()[option]
return module.versions
else:
raise InvalidArgumentError("{} is not a valid browser/platform with versions.\tValid options include : {}\n".format(option,VERSION_SUPPORTED_MODULES))

def generate(device: typing.Union[tuple, str, None] = None,
platform: typing.Union[tuple, str, None] = None,
browser: typing.Union[tuple, str, None] = None,
options: typing.Union[_options.Options, None] = None) -> user_agent.UserAgent:
global versions_idx_map
if options is not None and options.version_ranges is not None:
for option in options.version_ranges.keys():
"""initialize each index map for each browser/platform dynamically
and do this AT MOST across generates
for the same browser/platform where a version range is specified"""
if option in versions_idx_map and len(versions_idx_map[option]) == 0:
initialize_idx_map(option)
return user_agent.UserAgent(device, platform, browser, options)
10 changes: 6 additions & 4 deletions src/ua_generator/data/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@
"""

DEVICES = ('desktop', 'mobile')

PLATFORMS = ('windows', 'macos', 'ios', 'linux', 'android')
PLATFORMS = ('windows', 'macos', 'ios', 'linux', 'android', 'android_nexus','android_pixel','android_samsung')
PLATFORMS_DESKTOP = ('windows', 'macos', 'linux') # Platforms on desktop devices
PLATFORMS_MOBILE = ('ios', 'android') # Platforms on mobile devices

PLATFORMS_MOBILE = ('ios','android','android_nexus','android_pixel','android_samsung') # Platforms on mobile devices
VERSION_SUPPORTED_PLATFORMS_MOBILE = ('ios','android_nexus','android_pixel','android_samsung') #Platforms that support version filter on mobile
PLATFORMS_ANDROID = ('android_nexus','android_pixel','android_samsung')
ANDROIDS = ('android','android_nexus','android_pixel','android_samsung')
BROWSERS = ('chrome', 'edge', 'firefox', 'safari')
VERSION_SUPPORTED_MODULES = ('windows', 'macos', 'ios', 'linux', 'android_nexus','android_pixel','android_samsung','chrome', 'edge', 'firefox', 'safari')
BROWSERS_SUPPORT_CH = ('chrome', 'edge') # Browsers that support Client Hints
35 changes: 24 additions & 11 deletions src/ua_generator/data/browsers/chrome.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@

from ..version import Version, ChromiumVersion, VersionRange
from ...options import Options

from ...exceptions import InvalidVersionError
#ua-generator/src/data/browsers/chrome.py
# https://chromereleases.googleblog.com/search/label/Stable%20updates
versions: List[ChromiumVersion] = [
ChromiumVersion(Version(major=100, minor=0, build=4896, patch=(0, 255))),
Expand Down Expand Up @@ -40,20 +41,32 @@
ChromiumVersion(Version(major=127, minor=0, build=6533, patch=(0, 255))),
]

versions_idx_map = {}

def get_version(options: Options) -> ChromiumVersion:
selected_version : ChromiumVersion
if options.version_ranges is not None and 'chrome' in options.version_ranges:
if type(options.version_ranges['chrome']) == VersionRange:
filtered = options.version_ranges['chrome'].filter(versions)
if type(filtered) == list and len(filtered) > 0:
return random.choice(filtered)

weights = None
if options.weighted_versions:
version_range = options.version_ranges['chrome']
min_idx = 0
max_idx = len(versions)
if(version_range.min_version is not None):
if(version_range.min_version.major not in versions_idx_map):
raise InvalidVersionError("Invalid {} version {} specified, valid versions are {}-{}\n".format("firefox", version_range.min_version.major, versions[0].major, versions[-1].major))
min_idx = versions_idx_map[version_range.min_version.major]
if(version_range.max_version is not None):
if(version_range.max_version.major not in versions_idx_map):
raise InvalidVersionError("Invalid {} version {} specified, valid versions are {}-{}\n".format("firefox", version_range.min_version.major, versions[0].major, versions[-1].major))
max_idx = versions_idx_map[version_range.max_version.major]+1
filtered = versions[min_idx:max_idx]
if len(filtered) > 0:
selected_version = random.choice(filtered)
elif options.weighted_versions:
weights = [1.0] * len(versions)
weights[-1] = 10.0
weights[-2] = 9.0
weights[-3] = 8.0

choice: List[ChromiumVersion] = random.choices(versions, weights=weights, k=1)
return choice[0]
selected_version = random.choices(versions, weights=weights, k=1)[0]
else:
selected_version = random.choice(versions)
selected_version.get_version()
return selected_version
34 changes: 24 additions & 10 deletions src/ua_generator/data/browsers/edge.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@

from ..version import Version, ChromiumVersion, VersionRange
from ...options import Options
from ...exceptions import InvalidVersionError

#ua-generator/src/data/browsers/edge.py
# https://docs.microsoft.com/en-us/deployedge/microsoft-edge-release-schedule
versions: List[ChromiumVersion] = [
ChromiumVersion(Version(major=100, minor=0, build=1185, patch=(0, 99))),
Expand Down Expand Up @@ -42,20 +44,32 @@
ChromiumVersion(Version(major=128, minor=0, build=2739, patch=(0, 99))),
]

versions_idx_map = {}

def get_version(options: Options) -> ChromiumVersion:
selected_version : ChromiumVersion
if options.version_ranges is not None and 'edge' in options.version_ranges:
if type(options.version_ranges['edge']) == VersionRange:
filtered = options.version_ranges['edge'].filter(versions)
if type(filtered) == list and len(filtered) > 0:
return random.choice(filtered)

weights = None
if options.weighted_versions:
version_range = options.version_ranges['edge']
min_idx = 0
max_idx = len(versions)
if(version_range.min_version is not None):
if(version_range.min_version.major not in versions_idx_map):
raise InvalidVersionError("Invalid {} version {} specified, valid versions are {}-{}\n".format("firefox", version_range.min_version.major, versions[0].major, versions[-1].major))
min_idx = versions_idx_map[version_range.min_version.major]
if(version_range.max_version is not None):
if(version_range.max_version.major not in versions_idx_map):
raise InvalidVersionError("Invalid {} version {} specified, valid versions are {}-{}\n".format("firefox", version_range.min_version.major, versions[0].major, versions[-1].major))
max_idx = versions_idx_map[version_range.max_version.major]+1
filtered = versions[min_idx:max_idx]
if len(filtered) > 0:
selected_version = random.choice(filtered)
elif options.weighted_versions:
weights = [1.0] * len(versions)
weights[-1] = 10.0
weights[-2] = 9.0
weights[-3] = 8.0

choice: List[ChromiumVersion] = random.choices(versions, weights=weights, k=1)
return choice[0]
selected_version = random.choices(versions, weights=weights, k=1)[0]
else:
selected_version = random.choice(versions)
selected_version.get_version()
return selected_version
36 changes: 25 additions & 11 deletions src/ua_generator/data/browsers/firefox.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@

from ..version import Version, VersionRange
from ...options import Options

from ...exceptions import InvalidVersionError
#ua-generator/src/data/browsers/firefox.py
# https://www.mozilla.org/en-US/firefox/releases/
versions: List[Version] = [
Version(major=103, minor=0, build=(0, 2)),
Expand Down Expand Up @@ -49,20 +50,33 @@
Version(major=129, minor=0, build=0),
]

versions_idx_map = {}

def get_version(options: Options) -> Version:
selected_version : Version
if options.version_ranges is not None and 'firefox' in options.version_ranges:
if type(options.version_ranges['firefox']) == VersionRange:
filtered = options.version_ranges['firefox'].filter(versions)
if type(filtered) == list and len(filtered) > 0:
return random.choice(filtered)

weights = None
if options.weighted_versions:
version_range = options.version_ranges['firefox']
min_idx = 0
max_idx = len(versions)
if(version_range.min_version is not None):
if(version_range.min_version.major not in versions_idx_map):
raise InvalidVersionError("Invalid {} version {} specified, valid versions are {}-{}\n".format("firefox", version_range.min_version.major, versions[0].major, versions[-1].major))
min_idx = versions_idx_map[version_range.min_version.major]
if(version_range.max_version is not None):
if(version_range.max_version.major not in versions_idx_map):
raise InvalidVersionError("Invalid {} version {} specified, valid versions are {}-{}\n".format("firefox", version_range.min_version.major, versions[0].major, versions[-1].major))
max_idx = versions_idx_map[version_range.max_version.major]+1

filtered = versions[min_idx:max_idx]
if len(filtered) > 0:
selected_version = random.choice(filtered)
elif options.weighted_versions:
weights = [1.0] * len(versions)
weights[-1] = 10.0
weights[-2] = 9.0
weights[-3] = 8.0

choice: List[Version] = random.choices(versions, weights=weights, k=1)
return choice[0]
selected_version = random.choices(versions, weights=weights, k=1)[0]
else:
selected_version = random.choice(versions)
selected_version.get_version()
return selected_version
35 changes: 25 additions & 10 deletions src/ua_generator/data/browsers/safari.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
#ua-generator/src/data/browsers/safari.py
"""
Random User-Agent
Copyright: 2022-2024 Ekin Karadeniz (github.com/iamdual)
Expand All @@ -8,6 +9,7 @@

from ..version import Version, ChromiumVersion, VersionRange
from ...options import Options
from ...exceptions import InvalidVersionError

# https://developer.apple.com/documentation/safari-release-notes
versions: List[ChromiumVersion] = [
Expand All @@ -21,19 +23,32 @@
ChromiumVersion(Version(major=17, minor=(0, 6)), webkit=Version(major=605, minor=1, build=15)),
]

versions_idx_map = {}

def get_version(options: Options) -> ChromiumVersion:
selected_version : ChromiumVersion
if options.version_ranges is not None and 'safari' in options.version_ranges:
if type(options.version_ranges['safari']) == VersionRange:
filtered = options.version_ranges['safari'].filter(versions)
if type(filtered) == list and len(filtered) > 0:
return random.choice(filtered)

weights = None
if options.weighted_versions:
version_range = options.version_ranges['safari']
min_idx = 0
max_idx = len(versions)
if(version_range.min_version is not None):
if(version_range.min_version.major not in versions_idx_map):
raise InvalidVersionError("Invalid {} version {} specified, valid versions are {}-{}\n".format("firefox", version_range.min_version.major, versions[0].major, versions[-1].major))
min_idx = versions_idx_map[version_range.min_version.major]
if(version_range.max_version is not None):
if(version_range.max_version.major not in versions_idx_map):
raise InvalidVersionError("Invalid {} version {} specified, valid versions are {}-{}\n".format("firefox", version_range.min_version.major, versions[0].major, versions[-1].major))
max_idx = versions_idx_map[version_range.max_version.major]+1
filtered = versions[min_idx:max_idx]
if len(filtered) > 0:
selected_version = random.choice(filtered)

elif options.weighted_versions:
weights = [1.0] * len(versions)
weights[-1] = 10.0
weights[-2] = 9.0

choice: List[ChromiumVersion] = random.choices(versions, weights=weights, k=1)
return choice[0]
selected_version = random.choices(versions, weights=weights, k=1)[0]
else:
selected_version = random.choice(versions)
selected_version.get_version()
return selected_version
12 changes: 10 additions & 2 deletions src/ua_generator/data/generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
"""
from .browsers import chrome, safari, firefox, edge
from .platforms import ios, android, linux, windows, macos
from .platforms.android import android_nexus,android_pixel,android_samsung
from .. import utils, exceptions
from . import ANDROIDS
from ..options import Options


Expand All @@ -31,6 +33,12 @@ def __platform_version(self):
return linux.get_version(options=self.options)
elif self.platform == 'android':
return android.get_version(options=self.options)
elif self.platform == 'android_nexus':
return android_nexus.get_version(options=self.options)
elif self.platform == 'android_pixel':
return android_pixel.get_version(options=self.options)
elif self.platform == 'android_samsung':
return android_samsung.get_version(options=self.options)

def __browser_version(self):
if self.browser == 'chrome':
Expand Down Expand Up @@ -94,7 +102,7 @@ def __user_agent(self):
template = template.replace('{firefox}', str(self.browser_version))
return template

elif self.platform == 'android':
elif self.platform in ANDROIDS:
if self.browser == 'chrome':
template = 'Mozilla/5.0 (Linux; Android {android}{model}{build}) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/{chrome} Mobile Safari/{webkit}'
template = template.replace('{android}', str(self.platform_version.major))
Expand Down Expand Up @@ -173,4 +181,4 @@ def __user_agent(self):
template = template.replace('{firefox}', self.browser_version.format(partitions=2))
return template

raise exceptions.CannotGenerateError(self)
raise exceptions.CannotGenerateError("\n\nCould not generate UA with the following inputs:\nBrowser:{}\nPlatform:{}\nDevice:{}\nOptions:{}".format(self.browser,self.platform,self.device,str(self.options)))
38 changes: 28 additions & 10 deletions src/ua_generator/data/platforms/android/android_nexus.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@
import string
from typing import List

from ...version import Version, AndroidVersion
from ...version import Version, VersionRange, AndroidVersion
from ....options import Options
from ....exceptions import InvalidVersionError

# https://en.wikipedia.org/wiki/Android_version_history
# https://source.android.com/setup/start/build-numbers
Expand All @@ -33,24 +34,41 @@
]

platform_models = ('Nexus 5', 'Nexus 5X', 'Nexus 6', 'Nexus 6P', 'Nexus 9')

versions_idx_map = {}

def get_version(options: Options) -> AndroidVersion:
weights = None
if options.weighted_versions:
selected_version : AndroidVersion
if options.version_ranges is not None and 'android_nexus' in options.version_ranges:
version_range = options.version_ranges['android_nexus']
min_idx = 0
max_idx = len(versions)
if(version_range.min_version is not None):
if(version_range.min_version.major not in versions_idx_map):
raise InvalidVersionError("Invalid {} version {} specified, valid versions are {}-{}\n".format("firefox", version_range.min_version.major, versions[0].major, versions[-1].major))
min_idx = versions_idx_map[version_range.min_version.major]
if(version_range.max_version is not None):
if(version_range.max_version.major not in versions_idx_map):
raise InvalidVersionError("Invalid {} version {} specified, valid versions are {}-{}\n".format("firefox", version_range.min_version.major, versions[0].major, versions[-1].major))
max_idx = versions_idx_map[version_range.max_version.major]+1
filtered = versions[min_idx:max_idx]
if len(filtered) > 0:
selected_version = random.choice(filtered)
elif options.weighted_versions:
weights = [1.0] * len(versions)
weights[-1] = 10.0
weights[-2] = 9.0
weights[-3] = 8.0
weights[-4] = 8.0

choice: List[AndroidVersion] = random.choices(versions, weights=weights, k=1)

build_number = choice[0].build_number
selected_version = random.choices(versions, weights=weights, k=1)[0]
else:
selected_version = random.choice(versions)
selected_version.get_version()
build_number = selected_version.build_number
build_number = build_number.replace('{s}', '{}'.format(random.choice(string.ascii_uppercase)))
build_number = build_number.replace('{d}', '{:02d}{:02d}{:02d}'.format(random.randint(17, 22), random.randint(0, 12), random.randint(0, 29)))
build_number = build_number.replace('{v}', '{}'.format(random.randint(1, 255)))

choice[0].build_number = build_number
choice[0].platform_model = random.choice(platform_models)
return choice[0]
selected_version.build_number = build_number
selected_version.platform_model = random.choice(platform_models)
return selected_version
Loading