summaryrefslogtreecommitdiff
path: root/ffxiv/pystone
diff options
context:
space:
mode:
Diffstat (limited to 'ffxiv/pystone')
-rw-r--r--ffxiv/pystone/__init__.py0
-rw-r--r--ffxiv/pystone/character.py20
-rw-r--r--ffxiv/pystone/cli/client.py6
-rw-r--r--ffxiv/pystone/definition.py161
-rw-r--r--ffxiv/pystone/free_company.py5
-rw-r--r--ffxiv/pystone/lodestone.py39
-rw-r--r--ffxiv/pystone/types.py11
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]