Add BigBrother.
This commit is contained in:
parent
63832a3d8f
commit
fd14622a36
41
BigBrother/BigBrother.lua
Normal file
41
BigBrother/BigBrother.lua
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
BigBrother = {}
|
||||||
|
BigBrother.title = GetAddOnMetadata("BigBrother", "Title")
|
||||||
|
BigBrother.version = GetAddOnMetadata("BigBrother", "Version")
|
||||||
|
BigBrother.author = GetAddOnMetadata("BigBrother", "Author")
|
||||||
|
BigBrother.Items = {}
|
||||||
|
print(("Loaded %s v%s by %s."):format(BigBrother.title, BigBrother.version, BigBrother.author))
|
||||||
|
|
||||||
|
BigBrotherDB = {} -- default value, will be overwritten by the game if persistent data exists
|
||||||
|
|
||||||
|
local frame = CreateFrame("FRAME")
|
||||||
|
frame:RegisterEvent("ENCOUNTER_START")
|
||||||
|
frame:SetScript("OnEvent", function(self, event, encounterID, encounterName, difficultyID, groupSize)
|
||||||
|
if not IsInRaid() then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local classBuffs = {}
|
||||||
|
for className, _ in pairs(RAID_CLASS_COLORS) do
|
||||||
|
classBuffs[className] = {}
|
||||||
|
end
|
||||||
|
for m = 1, GetNumGroupMembers() do
|
||||||
|
local unit = "raid" .. m
|
||||||
|
local unitBuffs = {}
|
||||||
|
for b = 1, 40 do
|
||||||
|
_, _, _, _, _, _, _, _, _, spellId = UnitBuff(unit, b)
|
||||||
|
if not spellId then
|
||||||
|
break
|
||||||
|
end
|
||||||
|
unitBuffs[b] = spellId
|
||||||
|
end
|
||||||
|
local _, className = UnitClass(unit)
|
||||||
|
classBuffs[className][UnitName(unit)] = unitBuffs
|
||||||
|
end
|
||||||
|
|
||||||
|
table.insert(BigBrotherDB, {
|
||||||
|
date = date("%F %T"),
|
||||||
|
zone = GetRealZoneText(),
|
||||||
|
encounterID = encounterID,
|
||||||
|
encounterName = encounterName,
|
||||||
|
buffs = classBuffs,
|
||||||
|
})
|
||||||
|
end)
|
12
BigBrother/BigBrother.toc
Normal file
12
BigBrother/BigBrother.toc
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
## Interface: 11305
|
||||||
|
|
||||||
|
## Title: BigBrother
|
||||||
|
## Notes: Log raid buffs and consumables.
|
||||||
|
## Author: Caspervk
|
||||||
|
## Version: 0.0.1
|
||||||
|
|
||||||
|
## X-License: GNU General Public License v3 or later (GPLv3+)
|
||||||
|
|
||||||
|
##SavedVariables: BigBrotherDB
|
||||||
|
|
||||||
|
BigBrother.lua
|
0
BigBrother/README.md
Normal file
0
BigBrother/README.md
Normal file
4
BigBrother/bb/__init__.py
Normal file
4
BigBrother/bb/__init__.py
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
__version__ = "0.0.1"
|
||||||
|
__author__ = "Casper V. Kristensen"
|
||||||
|
__licence__ = "GPLv3"
|
||||||
|
__url__ = "https://git.caspervk.net/caspervk/wow-addons"
|
4
BigBrother/bb/__main__.py
Normal file
4
BigBrother/bb/__main__.py
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
if __name__ == "__main__":
|
||||||
|
from .cli import main
|
||||||
|
|
||||||
|
main()
|
69
BigBrother/bb/cli.py
Normal file
69
BigBrother/bb/cli.py
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
import argparse
|
||||||
|
import itertools
|
||||||
|
import logging
|
||||||
|
import logging.config
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
from . import __url__, __author__, __version__, config, graph
|
||||||
|
from .slpp import slpp as lua
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class CLI:
|
||||||
|
def __init__(self) -> None:
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
def parse_args(self):
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
prog="bb",
|
||||||
|
description=f"BigBrother v{__version__} by {__author__}.",
|
||||||
|
epilog=f"For more information, see <{__url__}>."
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
dest="saved_variables",
|
||||||
|
type=lambda s: Path(s),
|
||||||
|
help="Path to WTF/Account/<ACCOUNT>/SavedVariables/BigBrother.lua",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-v", "--verbose",
|
||||||
|
action="count",
|
||||||
|
default=0,
|
||||||
|
help="Increase verbosity level. Can be used multiple times."
|
||||||
|
)
|
||||||
|
return parser.parse_args()
|
||||||
|
|
||||||
|
def select_raid(self, saved_variables: Path) -> List[dict]:
|
||||||
|
text = saved_variables.read_text(encoding="utf8")
|
||||||
|
db = lua.decode(f"{{{text}}}") # double-{} since slpp requires all variables to be part of a lua table
|
||||||
|
raids = []
|
||||||
|
for zone, encounters in itertools.groupby(db["BigBrotherDB"], key=lambda x: (x["zone"], x["date"][:10])):
|
||||||
|
raids.append(list(encounters))
|
||||||
|
raids.reverse() # ensure latest raid is first
|
||||||
|
for i, raid in enumerate(raids, start=1):
|
||||||
|
print(f"{i:3} {raid[0]['date']} {raid[0]['zone']}")
|
||||||
|
choice = int(input("Raid: "))
|
||||||
|
return raids[choice-1]
|
||||||
|
|
||||||
|
def prompt_delete_saved_variables(self, saved_variables: Path) -> None:
|
||||||
|
if input("Delete saved variables BigBrother.lua? [y/N] ").lower() == "y":
|
||||||
|
saved_variables.unlink()
|
||||||
|
logger.info("%s deleted.", saved_variables)
|
||||||
|
|
||||||
|
def run(self) -> None:
|
||||||
|
args = self.parse_args()
|
||||||
|
logging.config.dictConfig(config.get_logging_config(level=("WARNING", "INFO", "DEBUG")[min(args.verbose, 2)]))
|
||||||
|
logger.debug("Args: %s", args)
|
||||||
|
raid = self.select_raid(args.saved_variables)
|
||||||
|
graph.graph(raid)
|
||||||
|
self.prompt_delete_saved_variables(args.saved_variables)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
cli = CLI()
|
||||||
|
cli.run()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
36
BigBrother/bb/config/__init__.py
Normal file
36
BigBrother/bb/config/__init__.py
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
import logging
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
here = Path(__file__).parent.resolve()
|
||||||
|
buffs = yaml.safe_load(here.joinpath("buffs.yaml").read_text("utf8"))
|
||||||
|
class_overrides = yaml.safe_load(here.joinpath("class_overrides.yaml").read_text("utf8"))
|
||||||
|
|
||||||
|
|
||||||
|
def get_logging_config(level="WARNING") -> dict:
|
||||||
|
return {
|
||||||
|
"version": 1,
|
||||||
|
"disable_existing_loggers": False,
|
||||||
|
"formatters": {
|
||||||
|
"standard": {
|
||||||
|
"format": "%(asctime)s [%(levelname)-7s] %(name)s:%(funcName)s - %(message)s"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"handlers": {
|
||||||
|
"console": {
|
||||||
|
"class": "logging.StreamHandler",
|
||||||
|
"stream": "ext://sys.stdout",
|
||||||
|
"formatter": "standard",
|
||||||
|
"level": level,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"loggers": {
|
||||||
|
"bb": {
|
||||||
|
"level": "DEBUG",
|
||||||
|
"handlers": ["console"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
131
BigBrother/bb/config/buffs.yaml
Normal file
131
BigBrother/bb/config/buffs.yaml
Normal file
|
@ -0,0 +1,131 @@
|
||||||
|
---
|
||||||
|
# 15366 Songflower
|
||||||
|
|
||||||
|
Dragonslayer:
|
||||||
|
color: "e20a00"
|
||||||
|
ids:
|
||||||
|
- 22888
|
||||||
|
classes:
|
||||||
|
- ALL
|
||||||
|
|
||||||
|
Zandalar:
|
||||||
|
color: "41ca46"
|
||||||
|
ids:
|
||||||
|
- 24425
|
||||||
|
classes:
|
||||||
|
- ALL
|
||||||
|
|
||||||
|
DM:T:
|
||||||
|
color: "016275"
|
||||||
|
ids:
|
||||||
|
- 22817 # Fengus' Ferocity
|
||||||
|
- 22820 # Slip'kik's Savvy
|
||||||
|
- 22818 # Mol'dar's Moxie
|
||||||
|
classes:
|
||||||
|
- ALL
|
||||||
|
|
||||||
|
Zanza:
|
||||||
|
color: "f07cf7"
|
||||||
|
ids:
|
||||||
|
- 24382 # Spirit of Zanza
|
||||||
|
- 20081 # Swiftness of Zanza
|
||||||
|
- 20080 # Sheen of Zanza
|
||||||
|
classes:
|
||||||
|
- ALL
|
||||||
|
|
||||||
|
|
||||||
|
Alchohol:
|
||||||
|
color: "b99863"
|
||||||
|
ids:
|
||||||
|
- 22790 # Kreeg's Stout Beatdown
|
||||||
|
- 22789 # Gordok Green Grog
|
||||||
|
- 25804 # Rumsey Rum Black Label
|
||||||
|
classes:
|
||||||
|
- Druid
|
||||||
|
- Priest
|
||||||
|
- Tank Warrior
|
||||||
|
|
||||||
|
Food:
|
||||||
|
color: "f6abaf"
|
||||||
|
ids:
|
||||||
|
- 18194 # Nightfin Soup
|
||||||
|
- 24799 # Smoked Desert Dumplings
|
||||||
|
- 18192 # Grilled Squid
|
||||||
|
- 18125 # Blessed Sunfruit
|
||||||
|
- 19710 # Tender Wolf Steak
|
||||||
|
- 22730 # Runn Tum Tuber Surprise
|
||||||
|
classes:
|
||||||
|
- ALL
|
||||||
|
|
||||||
|
Mageblood:
|
||||||
|
color: "550c65"
|
||||||
|
ids:
|
||||||
|
- 24363
|
||||||
|
classes:
|
||||||
|
- Balance Druid
|
||||||
|
- Druid
|
||||||
|
- Paladin
|
||||||
|
- Priest
|
||||||
|
|
||||||
|
Strength:
|
||||||
|
color: "d5bb33"
|
||||||
|
ids:
|
||||||
|
- 11405 # Elixir of the Giants
|
||||||
|
- 16323 # Juju Power
|
||||||
|
classes:
|
||||||
|
- Feral Druid
|
||||||
|
- Retribution Paladin
|
||||||
|
- Rogue
|
||||||
|
- Warrior
|
||||||
|
- Tank Warrior
|
||||||
|
|
||||||
|
Mongoose:
|
||||||
|
color: "5e66e5"
|
||||||
|
ids:
|
||||||
|
- 17538
|
||||||
|
classes:
|
||||||
|
- Feral Druid
|
||||||
|
- Hunter
|
||||||
|
- Retribution Paladin
|
||||||
|
- Rogue
|
||||||
|
- Warrior
|
||||||
|
- Tank Warrior
|
||||||
|
|
||||||
|
Arcane:
|
||||||
|
color: "9a39ad"
|
||||||
|
ids:
|
||||||
|
- 17539
|
||||||
|
classes:
|
||||||
|
- Balance Druid
|
||||||
|
- Mage
|
||||||
|
- Shadow Priest
|
||||||
|
- Warlock
|
||||||
|
|
||||||
|
Shadow:
|
||||||
|
color: "510164"
|
||||||
|
ids:
|
||||||
|
- 11474
|
||||||
|
classes:
|
||||||
|
- Shadow Priest
|
||||||
|
- Warlock
|
||||||
|
|
||||||
|
Frost:
|
||||||
|
color: "60d0fe"
|
||||||
|
ids:
|
||||||
|
- 21920
|
||||||
|
classes:
|
||||||
|
- Mage
|
||||||
|
|
||||||
|
Arthas:
|
||||||
|
color: "2bddc7"
|
||||||
|
ids:
|
||||||
|
- 11374
|
||||||
|
classes:
|
||||||
|
- Tank Warrior
|
||||||
|
|
||||||
|
Defense:
|
||||||
|
color: "0a0a0a"
|
||||||
|
ids:
|
||||||
|
- 11348
|
||||||
|
classes:
|
||||||
|
- Tank Warrior
|
10
BigBrother/bb/config/class_overrides.yaml
Normal file
10
BigBrother/bb/config/class_overrides.yaml
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
---
|
||||||
|
# Needed since Classic API doesn't support talent specialisation inspection
|
||||||
|
Ralfe: Balance Druid
|
||||||
|
Ezeriel: Feral Druid
|
||||||
|
Zyalar: Feral Druid
|
||||||
|
Borahk: Retribution Paladin
|
||||||
|
Loodt: Shadow Priest
|
||||||
|
Corten: Tank Warrior
|
||||||
|
Boldus: Tank Warrior
|
||||||
|
Exas: Tank Warrior
|
111
BigBrother/bb/graph.py
Normal file
111
BigBrother/bb/graph.py
Normal file
|
@ -0,0 +1,111 @@
|
||||||
|
import base64
|
||||||
|
import io
|
||||||
|
from collections import defaultdict
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
from . import config
|
||||||
|
|
||||||
|
|
||||||
|
def graph(raid: List[dict]) -> None:
|
||||||
|
figs = []
|
||||||
|
buff_ids = {
|
||||||
|
buff_id: buff_name
|
||||||
|
for buff_name, buff_data in config.buffs.items()
|
||||||
|
for buff_id in buff_data["ids"]
|
||||||
|
}
|
||||||
|
classes = defaultdict(lambda: defaultdict(lambda: defaultdict(list)))
|
||||||
|
xticks = []
|
||||||
|
first_encounter_date = None
|
||||||
|
last_encounter_date = None
|
||||||
|
previous_encounter_date = None
|
||||||
|
for encounter in raid:
|
||||||
|
encounter_date = datetime.strptime(encounter["date"], "%Y-%m-%d %H:%M:%S")
|
||||||
|
if first_encounter_date is None:
|
||||||
|
first_encounter_date = encounter_date
|
||||||
|
previous_encounter_date = encounter_date - timedelta(minutes=5)
|
||||||
|
last_encounter_date = encounter_date
|
||||||
|
xticks.append((encounter_date, encounter["encounterName"]))
|
||||||
|
for class_name, players in encounter["buffs"].items():
|
||||||
|
for player_name, buffs in sorted(players.items()):
|
||||||
|
real_class_name = config.class_overrides.get(player_name, str.title(class_name))
|
||||||
|
for buff_id in buffs:
|
||||||
|
try:
|
||||||
|
buff_name = buff_ids[buff_id]
|
||||||
|
except KeyError:
|
||||||
|
continue # buff not tracked
|
||||||
|
classes[real_class_name][player_name][buff_name].append(
|
||||||
|
(previous_encounter_date, (encounter_date - previous_encounter_date))
|
||||||
|
)
|
||||||
|
previous_encounter_date = encounter_date
|
||||||
|
|
||||||
|
for class_name, players in sorted(classes.items()):
|
||||||
|
fig, ax = plt.subplots()
|
||||||
|
required_class_buffs = {
|
||||||
|
buff_name: buff_data
|
||||||
|
for buff_name, buff_data in config.buffs.items()
|
||||||
|
if any(x in buff_data["classes"] for x in ("ALL", class_name))
|
||||||
|
}
|
||||||
|
#legend_handles = []
|
||||||
|
bar_width = 1 / (len(required_class_buffs) + 1)
|
||||||
|
for b, (buff_name, buff) in enumerate(required_class_buffs.items()):
|
||||||
|
bar_color = "#{color}".format(**buff)
|
||||||
|
#legend_handles.append(Patch(label=buff_name, color=bar_color))
|
||||||
|
for y0, (player_name, player_buffs) in zip(np.arange(len(players)), players.items()):
|
||||||
|
ax.broken_barh(
|
||||||
|
player_buffs[buff_name],
|
||||||
|
(bar_width/2 + y0 + b * bar_width, bar_width),
|
||||||
|
color=bar_color
|
||||||
|
)
|
||||||
|
|
||||||
|
annotation_color = "black"
|
||||||
|
try:
|
||||||
|
if player_buffs[buff_name][1][0] == first_encounter_date:
|
||||||
|
annotation_color = contrast_color(*tuple(int(buff["color"][i:i+2], 16) for i in (0, 2, 4)))
|
||||||
|
except IndexError:
|
||||||
|
pass
|
||||||
|
ax.annotate(
|
||||||
|
f" {buff_name}",
|
||||||
|
xy=(first_encounter_date, bar_width + y0 + b * bar_width),
|
||||||
|
ha="left",
|
||||||
|
va="center",
|
||||||
|
color=annotation_color
|
||||||
|
)
|
||||||
|
|
||||||
|
for y0 in np.arange(len(players)):
|
||||||
|
ax.axhline(y=y0, linewidth=1.0, color="black")
|
||||||
|
|
||||||
|
plt.xticks(*zip(*xticks), rotation=30, ha="right")
|
||||||
|
plt.yticks(
|
||||||
|
[x for x in np.arange(len(players))],
|
||||||
|
players
|
||||||
|
)
|
||||||
|
ax.invert_yaxis()
|
||||||
|
ax.grid(axis="x", linestyle="--")
|
||||||
|
ax.set_xlim(first_encounter_date - timedelta(minutes=10), last_encounter_date + timedelta(minutes=1))
|
||||||
|
|
||||||
|
plt.title(class_name, fontweight="bold", fontsize=16)
|
||||||
|
#plt.legend(handles=legend_handles, bbox_to_anchor=(1, 1))
|
||||||
|
fig.set_size_inches(15, 2*len(players))
|
||||||
|
plt.tight_layout()
|
||||||
|
#plt.show()
|
||||||
|
|
||||||
|
buffer = io.BytesIO()
|
||||||
|
fig.savefig(buffer, format="svg")
|
||||||
|
figs.append(base64.b64encode(buffer.getbuffer()).decode("ascii"))
|
||||||
|
|
||||||
|
with open("{date:10.10}-{zone}.html".format(**raid[0]), "w") as f:
|
||||||
|
f.write("<html>")
|
||||||
|
f.writelines(
|
||||||
|
f"<img src='data:image/svg+xml;base64,{fig}'/>"
|
||||||
|
for fig in figs
|
||||||
|
)
|
||||||
|
f.write("</html>")
|
||||||
|
|
||||||
|
|
||||||
|
def contrast_color(r, g, b):
|
||||||
|
luma = ((0.299 * r) + (0.587 * g) + (0.114 * b)) / 255
|
||||||
|
return "black" if luma > 0.5 else "white"
|
3
BigBrother/bb/requirements.txt
Normal file
3
BigBrother/bb/requirements.txt
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
pyyaml
|
||||||
|
matplotlib
|
||||||
|
numpy
|
281
BigBrother/bb/slpp.py
Normal file
281
BigBrother/bb/slpp.py
Normal file
|
@ -0,0 +1,281 @@
|
||||||
|
"""
|
||||||
|
Simple lua-python data structures parser from https://github.com/SirAnthony/slpp (b947496 @ 28-04-2020).
|
||||||
|
|
||||||
|
Copyright (c) 2010, 2011, 2012 SirAnthony <anthony at adsorbtion.org>
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in
|
||||||
|
all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
||||||
|
"""
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from numbers import Number
|
||||||
|
|
||||||
|
ERRORS = {
|
||||||
|
'unexp_end_string': u'Unexpected end of string while parsing Lua string.',
|
||||||
|
'unexp_end_table': u'Unexpected end of table while parsing Lua string.',
|
||||||
|
'mfnumber_minus': u'Malformed number (no digits after initial minus).',
|
||||||
|
'mfnumber_dec_point': u'Malformed number (no digits after decimal point).',
|
||||||
|
'mfnumber_sci': u'Malformed number (bad scientific format).',
|
||||||
|
}
|
||||||
|
|
||||||
|
def sequential(lst):
|
||||||
|
length = len(lst)
|
||||||
|
if length == 0 or lst[0] != 0:
|
||||||
|
return False
|
||||||
|
for i in range(length):
|
||||||
|
if i + 1 < length:
|
||||||
|
if lst[i] + 1 != lst[i+1]:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class ParseError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class SLPP(object):
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.text = ''
|
||||||
|
self.ch = ''
|
||||||
|
self.at = 0
|
||||||
|
self.len = 0
|
||||||
|
self.depth = 0
|
||||||
|
self.space = re.compile('\s', re.M)
|
||||||
|
self.alnum = re.compile('\w', re.M)
|
||||||
|
self.newline = '\n'
|
||||||
|
self.tab = '\t'
|
||||||
|
|
||||||
|
def decode(self, text):
|
||||||
|
if not text or not isinstance(text, str):
|
||||||
|
return
|
||||||
|
# FIXME: only short comments removed
|
||||||
|
reg = re.compile('--.*$', re.M)
|
||||||
|
text = reg.sub('', text, 0)
|
||||||
|
self.text = text
|
||||||
|
self.at, self.ch, self.depth = 0, '', 0
|
||||||
|
self.len = len(text)
|
||||||
|
self.next_chr()
|
||||||
|
result = self.value()
|
||||||
|
return result
|
||||||
|
|
||||||
|
def encode(self, obj):
|
||||||
|
self.depth = 0
|
||||||
|
return self.__encode(obj)
|
||||||
|
|
||||||
|
def __encode(self, obj):
|
||||||
|
s = ''
|
||||||
|
tab = self.tab
|
||||||
|
newline = self.newline
|
||||||
|
|
||||||
|
if isinstance(obj, str):
|
||||||
|
s += '"%s"' % obj.replace(r'"', r'\"')
|
||||||
|
elif isinstance(obj, bytes):
|
||||||
|
s += '"{}"'.format(''.join(r'\x{:02x}'.format(c) for c in obj))
|
||||||
|
elif isinstance(obj, bool):
|
||||||
|
s += str(obj).lower()
|
||||||
|
elif obj is None:
|
||||||
|
s += 'nil'
|
||||||
|
elif isinstance(obj, Number):
|
||||||
|
s += str(obj)
|
||||||
|
elif isinstance(obj, (list, tuple, dict)):
|
||||||
|
self.depth += 1
|
||||||
|
if len(obj) == 0 or (not isinstance(obj, dict) and len([
|
||||||
|
x for x in obj
|
||||||
|
if isinstance(x, Number) or (isinstance(x, str) and len(x) < 10)
|
||||||
|
]) == len(obj)):
|
||||||
|
newline = tab = ''
|
||||||
|
dp = tab * self.depth
|
||||||
|
s += "%s{%s" % (tab * (self.depth - 2), newline)
|
||||||
|
if isinstance(obj, dict):
|
||||||
|
key = '[%s]' if all(isinstance(k, int) for k in obj.keys()) else '%s'
|
||||||
|
contents = [dp + (key + ' = %s') % (k, self.__encode(v)) for k, v in obj.items()]
|
||||||
|
s += (',%s' % newline).join(contents)
|
||||||
|
else:
|
||||||
|
s += (',%s' % newline).join(
|
||||||
|
[dp + self.__encode(el) for el in obj])
|
||||||
|
self.depth -= 1
|
||||||
|
s += "%s%s}" % (newline, tab * self.depth)
|
||||||
|
return s
|
||||||
|
|
||||||
|
def white(self):
|
||||||
|
while self.ch:
|
||||||
|
if self.space.match(self.ch):
|
||||||
|
self.next_chr()
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
|
||||||
|
def next_chr(self):
|
||||||
|
if self.at >= self.len:
|
||||||
|
self.ch = None
|
||||||
|
return None
|
||||||
|
self.ch = self.text[self.at]
|
||||||
|
self.at += 1
|
||||||
|
return True
|
||||||
|
|
||||||
|
def value(self):
|
||||||
|
self.white()
|
||||||
|
if not self.ch:
|
||||||
|
return
|
||||||
|
if self.ch == '{':
|
||||||
|
return self.object()
|
||||||
|
if self.ch == "[":
|
||||||
|
self.next_chr()
|
||||||
|
if self.ch in ['"', "'", '[']:
|
||||||
|
return self.string(self.ch)
|
||||||
|
if self.ch.isdigit() or self.ch == '-':
|
||||||
|
return self.number()
|
||||||
|
return self.word()
|
||||||
|
|
||||||
|
def string(self, end=None):
|
||||||
|
s = ''
|
||||||
|
start = self.ch
|
||||||
|
if end == '[':
|
||||||
|
end = ']'
|
||||||
|
if start in ['"', "'", '[']:
|
||||||
|
while self.next_chr():
|
||||||
|
if self.ch == end:
|
||||||
|
self.next_chr()
|
||||||
|
if start != "[" or self.ch == ']':
|
||||||
|
return s
|
||||||
|
if self.ch == '\\' and start == end:
|
||||||
|
self.next_chr()
|
||||||
|
if self.ch != end:
|
||||||
|
s += '\\'
|
||||||
|
s += self.ch
|
||||||
|
raise ParseError(ERRORS['unexp_end_string'])
|
||||||
|
|
||||||
|
def object(self):
|
||||||
|
o = {}
|
||||||
|
k = None
|
||||||
|
idx = 0
|
||||||
|
numeric_keys = False
|
||||||
|
self.depth += 1
|
||||||
|
self.next_chr()
|
||||||
|
self.white()
|
||||||
|
if self.ch and self.ch == '}':
|
||||||
|
self.depth -= 1
|
||||||
|
self.next_chr()
|
||||||
|
return o # Exit here
|
||||||
|
else:
|
||||||
|
while self.ch:
|
||||||
|
self.white()
|
||||||
|
if self.ch == '{':
|
||||||
|
o[idx] = self.object()
|
||||||
|
idx += 1
|
||||||
|
continue
|
||||||
|
elif self.ch == '}':
|
||||||
|
self.depth -= 1
|
||||||
|
self.next_chr()
|
||||||
|
if k is not None:
|
||||||
|
o[idx] = k
|
||||||
|
if len([key for key in o if isinstance(key, (str, float, bool, tuple))]) == 0:
|
||||||
|
so = sorted([key for key in o])
|
||||||
|
if sequential(so):
|
||||||
|
ar = []
|
||||||
|
for key in o:
|
||||||
|
ar.insert(key, o[key])
|
||||||
|
o = ar
|
||||||
|
return o # or here
|
||||||
|
else:
|
||||||
|
if self.ch == ',':
|
||||||
|
self.next_chr()
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
k = self.value()
|
||||||
|
if self.ch == ']':
|
||||||
|
self.next_chr()
|
||||||
|
self.white()
|
||||||
|
ch = self.ch
|
||||||
|
if ch in ('=', ','):
|
||||||
|
self.next_chr()
|
||||||
|
self.white()
|
||||||
|
if ch == '=':
|
||||||
|
o[k] = self.value()
|
||||||
|
else:
|
||||||
|
o[idx] = k
|
||||||
|
idx += 1
|
||||||
|
k = None
|
||||||
|
raise ParseError(ERRORS['unexp_end_table']) # Bad exit here
|
||||||
|
|
||||||
|
words = {'true': True, 'false': False, 'nil': None}
|
||||||
|
def word(self):
|
||||||
|
s = ''
|
||||||
|
if self.ch != '\n':
|
||||||
|
s = self.ch
|
||||||
|
self.next_chr()
|
||||||
|
while self.ch is not None and self.alnum.match(self.ch) and s not in self.words:
|
||||||
|
s += self.ch
|
||||||
|
self.next_chr()
|
||||||
|
return self.words.get(s, s)
|
||||||
|
|
||||||
|
def number(self):
|
||||||
|
def next_digit(err):
|
||||||
|
n = self.ch
|
||||||
|
self.next_chr()
|
||||||
|
if not self.ch or not self.ch.isdigit():
|
||||||
|
raise ParseError(err)
|
||||||
|
return n
|
||||||
|
n = ''
|
||||||
|
try:
|
||||||
|
if self.ch == '-':
|
||||||
|
n += next_digit(ERRORS['mfnumber_minus'])
|
||||||
|
n += self.digit()
|
||||||
|
if n == '0' and self.ch in ['x', 'X']:
|
||||||
|
n += self.ch
|
||||||
|
self.next_chr()
|
||||||
|
n += self.hex()
|
||||||
|
else:
|
||||||
|
if self.ch and self.ch == '.':
|
||||||
|
n += next_digit(ERRORS['mfnumber_dec_point'])
|
||||||
|
n += self.digit()
|
||||||
|
if self.ch and self.ch in ['e', 'E']:
|
||||||
|
n += self.ch
|
||||||
|
self.next_chr()
|
||||||
|
if not self.ch or self.ch not in ('+', '-'):
|
||||||
|
raise ParseError(ERRORS['mfnumber_sci'])
|
||||||
|
n += next_digit(ERRORS['mfnumber_sci'])
|
||||||
|
n += self.digit()
|
||||||
|
except ParseError:
|
||||||
|
t, e = sys.exc_info()[:2]
|
||||||
|
print(e)
|
||||||
|
return 0
|
||||||
|
try:
|
||||||
|
return int(n, 0)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
return float(n)
|
||||||
|
|
||||||
|
def digit(self):
|
||||||
|
n = ''
|
||||||
|
while self.ch and self.ch.isdigit():
|
||||||
|
n += self.ch
|
||||||
|
self.next_chr()
|
||||||
|
return n
|
||||||
|
|
||||||
|
def hex(self):
|
||||||
|
n = ''
|
||||||
|
while self.ch and (self.ch in 'ABCDEFabcdef' or self.ch.isdigit()):
|
||||||
|
n += self.ch
|
||||||
|
self.next_chr()
|
||||||
|
return n
|
||||||
|
|
||||||
|
|
||||||
|
slpp = SLPP()
|
||||||
|
|
||||||
|
__all__ = ['slpp']
|
Loading…
Reference in a new issue