diff options
| author | lexicade <jasonnlelong@gmail.com> | 2023-01-27 21:06:30 +0000 | 
|---|---|---|
| committer | lexicade <jasonnlelong@gmail.com> | 2023-01-27 21:06:30 +0000 | 
| commit | 52801b4de1d63cd01191acf7fcee137977140ec0 (patch) | |
| tree | 08271a1f1e3e8060486b6651c67c9934867c648e /ffxiv/pystone | |
| parent | 8df873808c86805624851356f5dea76ec621de23 (diff) | |
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] | 
