diff options
Diffstat (limited to 'ffxiv/pystone')
-rw-r--r-- | ffxiv/pystone/__init__.py | 0 | ||||
-rw-r--r-- | ffxiv/pystone/character.py | 20 | ||||
-rw-r--r-- | ffxiv/pystone/cli/client.py | 6 | ||||
-rw-r--r-- | ffxiv/pystone/definition.py | 161 | ||||
-rw-r--r-- | ffxiv/pystone/free_company.py | 5 | ||||
-rw-r--r-- | ffxiv/pystone/lodestone.py | 39 | ||||
-rw-r--r-- | ffxiv/pystone/types.py | 11 |
7 files changed, 242 insertions, 0 deletions
diff --git a/ffxiv/pystone/__init__.py b/ffxiv/pystone/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/ffxiv/pystone/__init__.py diff --git a/ffxiv/pystone/character.py b/ffxiv/pystone/character.py new file mode 100644 index 0000000..200baeb --- /dev/null +++ b/ffxiv/pystone/character.py @@ -0,0 +1,20 @@ +from typing import List, Optional, Tuple + +from requests import Session + +from pystone.definition import Definition + + +class Character: + def __init__(self, *, definitions: List[Definition]): + self.definitions = {(x.name): x for x in definitions} + + def __getattr__(self, name): + if name in self.definitions: + return self.definitions[name] + + def to_json(self): + json = {} + for definition in self.definitions.values(): + json.update(definition.to_json()) + return json diff --git a/ffxiv/pystone/cli/client.py b/ffxiv/pystone/cli/client.py new file mode 100644 index 0000000..1f31f58 --- /dev/null +++ b/ffxiv/pystone/cli/client.py @@ -0,0 +1,6 @@ +# from sys import argv + +import pystone + +if __name__ == '__main__': + print(dir(pystone)) diff --git a/ffxiv/pystone/definition.py b/ffxiv/pystone/definition.py new file mode 100644 index 0000000..3b4c5eb --- /dev/null +++ b/ffxiv/pystone/definition.py @@ -0,0 +1,161 @@ +from pathlib import Path +from typing import Dict, Union, Generic, TypeVar, Optional +from json import loads +from re import compile + +from bs4 import BeautifulSoup +from requests import Session + +T = TypeVar('T') + + +class Reference(Generic[T]): + """Represents a reference to something which may not exist yet.""" + def __init__(self, initial_value: Optional[T] = None): + self._value = initial_value + + @property + def value(self) -> Optional[T]: + return self._value + + @value.setter + def value(self, new_value: T): + self._value = new_value + + +class Element: + """An element is something that has a selector property with other optional properties that refine that selection""" + def __init__(self, name: str, data: Dict[str, str]): + self.name = name + self.selector = data['selector'] # I want this to error + self.regex = None + self.attribute = None + + if 'regex' in data: + self.regex = compile(data['regex']) + elif 'attribute' in data: + self.attribute = data['attribute'] + + def process(self, soup: BeautifulSoup) -> str: + selection = soup.select_one(self.selector) + if self.attribute is not None: + # TODO: this is fragile; fix + try: + text = selection[self.attribute] + except TypeError: # NoneType + text = '' + else: + try: + text = selection.text + except AttributeError: # NoneType + text = '' + + if self.regex is not None: + # TODO: this is fragile; fix + try: + return self.regex.search(text).group(1) + except AttributeError: # NoneType + return '' + else: + return text + + def __repr__(self): + return f'<Element:{self.name}>' + + +class Container: + """A container contains multiple elements or even other containers""" + def __init__(self, name: str, soup_ref: Reference[BeautifulSoup] = Reference()): + self.name = name + self.entries = {} + self.soup_ref = soup_ref + self.selector_root = None + + def add(self, name: str, data: Union['Container', Element]): + # TODO: raise error on overwriting key + self.entries[name] = data + + def __getattr__(self, name): + if name in self.entries: + entry = self.entries[name] + if isinstance(entry, Element): + return entry.process(self.soup_ref.value) + return self.entries[name] + + def __iter__(self): + def internal_iterator(): + for entry in self.entries: + yield entry + return internal_iterator() + + def to_json(self): + json = {self.name: {}} + for entry in self.entries.values(): + if isinstance(entry, Element): + json[self.name].update({entry.name: entry.process(self.soup_ref.value)}) + else: # container + json[self.name].update({entry.name: entry.to_json()}) + + return json + + def set_selector_root(self, root): + self.selector_root = root + + def contains(self): + """returns a list of everything this container contains""" + return self.entries.keys() + + def __dir__(self): + return self.entries.keys() + + def __repr__(self): + return f'<Container:{self.name}>' + + +class Definition: + """Takes in a json definition file and stores its name/definition""" + def __init__(self, path: Union[str, Path], fmt_url: str, *, session: Optional[Session] = Session()): + if isinstance(path, str): + path = Path(path) + if path.suffix != '.json': + raise Exception('something is wrong.. why is this loading a non-json file?') + self.fmt_url = fmt_url + self.name = path.stem + self.tree = Container(self.name) + self.session = session + + with open(path.expanduser()) as f: + json_data = loads(f.read()) + self._build_tree(json_data, self.tree) + + def _build_tree(self, json_data, root: Container): + for k, v in json_data.items(): + if 'selector' in v: + # we're making an element to add to our container + root.add(k.lower(), Element( + k.lower(), + v + )) + else: + # build a new Container and recurse + c = Container(k.lower()) + # if 'ROOT' in k: + # selector_root = k['ROOT']['selector'] + + self._build_tree(v, root=c) + root.add(k.lower(), c) + + def process(self, vars: Dict[str, str]): + response = self.session.get( + self.fmt_url % vars + ) + response.raise_for_status() + with open(self.name + '.html', 'w', encoding='utf-8') as f: + f.write(response.text) + self.tree.soup_ref.value = BeautifulSoup(response.text, features="html.parser") + + def to_json(self): + return self.tree.to_json() + + def __getattr__(self, name): + return getattr(self.tree, name) diff --git a/ffxiv/pystone/free_company.py b/ffxiv/pystone/free_company.py new file mode 100644 index 0000000..4560fb3 --- /dev/null +++ b/ffxiv/pystone/free_company.py @@ -0,0 +1,5 @@ +from pystone.definition import Definition + +class FCDefinition(Definition): + """FC-specific implementation of a definition""" + # don't make me do this Kara
\ No newline at end of file diff --git a/ffxiv/pystone/lodestone.py b/ffxiv/pystone/lodestone.py new file mode 100644 index 0000000..3f48c2b --- /dev/null +++ b/ffxiv/pystone/lodestone.py @@ -0,0 +1,39 @@ +from typing import Union, List +from pathlib import Path +from json import loads + +from requests_cache import CachedSession as Session +# from requests import Session + +from pystone.types import MetaDict +from pystone.definition import Definition +from pystone.character import Character + +LODESTONE_BASE_URL = 'finalfantasyxiv.com/lodestone' + + +class Lodestone: + def __init__(self, *, json_base: Union[str, Path]): + if isinstance(json_base, str): + json_base = Path(json_base) + self.json_base: Path = json_base + + # read meta.json + with open(json_base / 'meta.json') as f: + self.meta: MetaDict = loads(f.read()) + self.session = Session() + # self.session.headers.update({ + # 'User-Agent': self.meta['userAgentDesktop'] + # }) + + def get_character_by_id(self, id: Union[str, int]) -> None: + profile_json_files = (self.json_base / 'profile').expanduser().glob('*.json') + definitions: List[Definition] = [] + + for profile in profile_json_files: + url = self.meta['applicableUris'][f'profile/{profile.name}'] + d = Definition(profile, url, session=self.session) + d.process({'id': str(id)}) + definitions.append(d) + + return Character(definitions=definitions) diff --git a/ffxiv/pystone/types.py b/ffxiv/pystone/types.py new file mode 100644 index 0000000..5fa7da6 --- /dev/null +++ b/ffxiv/pystone/types.py @@ -0,0 +1,11 @@ +from typing import Dict, Literal, Union, TypedDict + +LangOptions = Literal['en'] +Definition = Dict[str, Union[str, Dict[str, str]]] + + +class MetaDict(TypedDict): + version: str + userAgentDesktop: str + userAgentMobile: str + applicableUris: Dict[str, str] |