[wip] move files in order to have a clean directory structure
This commit is contained in:
parent
8aebeb6346
commit
41f7dc2456
31 changed files with 0 additions and 0 deletions
35
nemubot/tools/__init__.py
Normal file
35
nemubot/tools/__init__.py
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Nemubot is a smart and modulable IM bot.
|
||||
# Copyright (C) 2012-2014 nemunaire
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import imp
|
||||
|
||||
|
||||
def reload():
|
||||
import tools.countdown
|
||||
imp.reload(tools.countdown)
|
||||
|
||||
import tools.date
|
||||
imp.reload(tools.date)
|
||||
|
||||
import tools.web
|
||||
imp.reload(tools.web)
|
||||
|
||||
import tools.xmlparser
|
||||
imp.reload(tools.xmlparser)
|
||||
import tools.xmlparser.node
|
||||
imp.reload(tools.xmlparser.node)
|
||||
132
nemubot/tools/config.py
Normal file
132
nemubot/tools/config.py
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Nemubot is a modulable IRC bot, built around XML configuration files.
|
||||
# Copyright (C) 2012 Mercier Pierre-Olivier
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import logging
|
||||
import os
|
||||
|
||||
from tools.xmlparser import parse_file
|
||||
|
||||
logger = logging.getLogger("nemubot.tools.config")
|
||||
|
||||
|
||||
def get_boolean(d, k, default=False):
|
||||
return ((k in d and d[k].lower() != "false" and d[k].lower() != "off") or
|
||||
(k not in d and default))
|
||||
|
||||
|
||||
def _load_server(config, xmlnode):
|
||||
"""Load a server configuration
|
||||
|
||||
Arguments:
|
||||
config -- the global configuration
|
||||
xmlnode -- the current server configuration node
|
||||
"""
|
||||
|
||||
opts = {
|
||||
"host": xmlnode["host"],
|
||||
"ssl": xmlnode.hasAttribute("ssl") and xmlnode["ssl"].lower() == "true",
|
||||
|
||||
"nick": xmlnode["nick"] if xmlnode.hasAttribute("nick") else config["nick"],
|
||||
"owner": xmlnode["owner"] if xmlnode.hasAttribute("owner") else config["owner"],
|
||||
}
|
||||
|
||||
# Optional keyword arguments
|
||||
for optional_opt in [ "port", "username", "realname",
|
||||
"password", "encoding", "caps" ]:
|
||||
if xmlnode.hasAttribute(optional_opt):
|
||||
opts[optional_opt] = xmlnode[optional_opt]
|
||||
elif optional_opt in config:
|
||||
opts[optional_opt] = config[optional_opt]
|
||||
|
||||
# Command to send on connection
|
||||
if "on_connect" in xmlnode:
|
||||
def on_connect():
|
||||
yield xmlnode["on_connect"]
|
||||
opts["on_connect"] = on_connect
|
||||
|
||||
# Channels to autojoin on connection
|
||||
if xmlnode.hasNode("channel"):
|
||||
opts["channels"] = list()
|
||||
for chn in xmlnode.getNodes("channel"):
|
||||
opts["channels"].append((chn["name"], chn["password"])
|
||||
if chn["password"] is not None
|
||||
else chn["name"])
|
||||
|
||||
# Server/client capabilities
|
||||
if "caps" in xmlnode or "caps" in config:
|
||||
capsl = (xmlnode["caps"] if xmlnode.hasAttribute("caps")
|
||||
else config["caps"]).lower()
|
||||
if capsl == "no" or capsl == "off" or capsl == "false":
|
||||
opts["caps"] = None
|
||||
else:
|
||||
opts["caps"] = capsl.split(',')
|
||||
else:
|
||||
opts["caps"] = list()
|
||||
|
||||
# Bind the protocol asked to the corresponding implementation
|
||||
if "protocol" not in xmlnode or xmlnode["protocol"] == "irc":
|
||||
from server.IRC import IRC as IRCServer
|
||||
srvcls = IRCServer
|
||||
else:
|
||||
raise Exception("Unhandled protocol '%s'" %
|
||||
xmlnode["protocol"])
|
||||
|
||||
# Initialize the server
|
||||
return srvcls(**opts)
|
||||
|
||||
|
||||
def load_file(filename, context):
|
||||
"""Load the configuration file
|
||||
|
||||
Arguments:
|
||||
filename -- the path to the file to load
|
||||
"""
|
||||
|
||||
if os.path.isfile(filename):
|
||||
config = parse_file(filename)
|
||||
|
||||
# This is a true nemubot configuration file, load it!
|
||||
if config.getName() == "nemubotconfig":
|
||||
# Preset each server in this file
|
||||
for server in config.getNodes("server"):
|
||||
srv = _load_server(config, server)
|
||||
|
||||
# Add the server in the context
|
||||
if context.add_server(srv, get_boolean(server, "autoconnect")):
|
||||
print("Server '%s' successfully added." % srv.id)
|
||||
else:
|
||||
print("Can't add server '%s'." % srv.id)
|
||||
|
||||
# Load module and their configuration
|
||||
for mod in config.getNodes("module"):
|
||||
context.modules_configuration[mod["name"]] = mod
|
||||
if get_boolean(mod, "autoload", default=True):
|
||||
__import__(mod["name"])
|
||||
|
||||
# Load files asked by the configuration file
|
||||
for load in config.getNodes("include"):
|
||||
load_file(load["path"], context)
|
||||
|
||||
# Other formats
|
||||
else:
|
||||
print (" Can't load `%s'; this is not a valid nemubot "
|
||||
"configuration file." % filename)
|
||||
|
||||
# Unexisting file, assume a name was passed, import the module!
|
||||
else:
|
||||
context.import_module(filename)
|
||||
108
nemubot/tools/countdown.py
Normal file
108
nemubot/tools/countdown.py
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Nemubot is a smart and modulable IM bot.
|
||||
# Copyright (C) 2012-2014 nemunaire
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from datetime import datetime, timezone
|
||||
import time
|
||||
|
||||
|
||||
def countdown(delta, resolution=5):
|
||||
sec = delta.seconds
|
||||
hours, remainder = divmod(sec, 3600)
|
||||
minutes, seconds = divmod(remainder, 60)
|
||||
an = int(delta.days / 365.25)
|
||||
days = delta.days % 365.25
|
||||
|
||||
sentence = ""
|
||||
force = False
|
||||
|
||||
if resolution > 0 and (force or an > 0):
|
||||
force = True
|
||||
sentence += " %i an" % an
|
||||
|
||||
if an > 1:
|
||||
sentence += "s"
|
||||
if resolution > 2:
|
||||
sentence += ","
|
||||
elif resolution > 1:
|
||||
sentence += " et"
|
||||
|
||||
if resolution > 1 and (force or days > 0):
|
||||
force = True
|
||||
sentence += " %i jour" % days
|
||||
|
||||
if days > 1:
|
||||
sentence += "s"
|
||||
if resolution > 3:
|
||||
sentence += ","
|
||||
elif resolution > 2:
|
||||
sentence += " et"
|
||||
|
||||
if resolution > 2 and (force or hours > 0):
|
||||
force = True
|
||||
sentence += " %i heure" % hours
|
||||
if hours > 1:
|
||||
sentence += "s"
|
||||
if resolution > 4:
|
||||
sentence += ","
|
||||
elif resolution > 3:
|
||||
sentence += " et"
|
||||
|
||||
if resolution > 3 and (force or minutes > 0):
|
||||
force = True
|
||||
sentence += " %i minute" % minutes
|
||||
if minutes > 1:
|
||||
sentence += "s"
|
||||
if resolution > 4:
|
||||
sentence += " et"
|
||||
|
||||
if resolution > 4 and (force or seconds > 0):
|
||||
force = True
|
||||
sentence += " %i seconde" % seconds
|
||||
if seconds > 1:
|
||||
sentence += "s"
|
||||
return sentence[1:]
|
||||
|
||||
|
||||
def countdown_format(date, msg_before, msg_after, tz=None):
|
||||
"""Replace in a text %s by a sentence incidated the remaining time
|
||||
before/after an event"""
|
||||
if tz is not None:
|
||||
oldtz = os.environ['TZ']
|
||||
os.environ['TZ'] = tz
|
||||
time.tzset()
|
||||
|
||||
# Calculate time before the date
|
||||
try:
|
||||
if datetime.now(timezone.utc) > date:
|
||||
sentence_c = msg_after
|
||||
delta = datetime.now(timezone.utc) - date
|
||||
else:
|
||||
sentence_c = msg_before
|
||||
delta = date - datetime.now(timezone.utc)
|
||||
except TypeError:
|
||||
if datetime.now() > date:
|
||||
sentence_c = msg_after
|
||||
delta = datetime.now() - date
|
||||
else:
|
||||
sentence_c = msg_before
|
||||
delta = date - datetime.now()
|
||||
|
||||
if tz is not None:
|
||||
os.environ['TZ'] = oldtz
|
||||
|
||||
return sentence_c % countdown(delta)
|
||||
90
nemubot/tools/date.py
Normal file
90
nemubot/tools/date.py
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Nemubot is a smart and modulable IM bot.
|
||||
# Copyright (C) 2012-2014 nemunaire
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# Extraction/Format text
|
||||
|
||||
from datetime import datetime, date
|
||||
import re
|
||||
|
||||
xtrdt = re.compile(r'''^.*? (?P<day>[0-9]{1,4}) .+?
|
||||
(?P<month>[0-9]{1,2}|janvier|january|fevrier|février|february|mars|march|avril|april|mai|maï|may|juin|juni|juillet|july|jully|august|aout|août|septembre|september|october|octobre|oktober|novembre|november|decembre|décembre|december)
|
||||
(?:.+?(?P<year>[0-9]{1,4}))? (?:[^0-9]+
|
||||
(?:(?P<hour>[0-9]{1,2})[^0-9]*[h':]
|
||||
(?:[^0-9]*(?P<minute>[0-9]{1,2})
|
||||
(?:[^0-9]*[m\":][^0-9]*(?P<second>[0-9]{1,2}))?)?)?.*?)?
|
||||
$''', re.X)
|
||||
|
||||
|
||||
def extractDate(msg):
|
||||
"""Parse a message to extract a time and date"""
|
||||
result = xtrdt.match(msg.lower())
|
||||
if result is not None:
|
||||
day = result.group("day")
|
||||
month = result.group("month")
|
||||
if month == "janvier" or month == "january" or month == "januar":
|
||||
month = 1
|
||||
elif month == "fevrier" or month == "février" or month == "february":
|
||||
month = 2
|
||||
elif month == "mars" or month == "march":
|
||||
month = 3
|
||||
elif month == "avril" or month == "april":
|
||||
month = 4
|
||||
elif month == "mai" or month == "may" or month == "maï":
|
||||
month = 5
|
||||
elif month == "juin" or month == "juni" or month == "junni":
|
||||
month = 6
|
||||
elif month == "juillet" or month == "jully" or month == "july":
|
||||
month = 7
|
||||
elif month == "aout" or month == "août" or month == "august":
|
||||
month = 8
|
||||
elif month == "september" or month == "septembre":
|
||||
month = 9
|
||||
elif month == "october" or month == "october" or month == "oktober":
|
||||
month = 10
|
||||
elif month == "november" or month == "novembre":
|
||||
month = 11
|
||||
elif month == "december" or month == "decembre" or month == "décembre":
|
||||
month = 12
|
||||
|
||||
year = result.group("year")
|
||||
|
||||
if len(day) == 4:
|
||||
day, year = year, day
|
||||
|
||||
hour = result.group("hour")
|
||||
minute = result.group("minute")
|
||||
second = result.group("second")
|
||||
|
||||
if year is None:
|
||||
year = date.today().year
|
||||
if hour is None:
|
||||
hour = 0
|
||||
if minute is None:
|
||||
minute = 0
|
||||
if second is None:
|
||||
second = 1
|
||||
else:
|
||||
second = int(second) + 1
|
||||
if second > 59:
|
||||
minute = int(minute) + 1
|
||||
second = 0
|
||||
|
||||
return datetime(int(year), int(month), int(day),
|
||||
int(hour), int(minute), int(second))
|
||||
else:
|
||||
return None
|
||||
38
nemubot/tools/human.py
Normal file
38
nemubot/tools/human.py
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Nemubot is a modulable IRC bot, built around XML configuration files.
|
||||
# Copyright (C) 2012 Mercier Pierre-Olivier
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import math
|
||||
|
||||
def size(size, unit=True):
|
||||
"""Convert a given byte size to an more human readable way
|
||||
|
||||
Argument:
|
||||
size -- the size to convert
|
||||
unit -- append the unit at the end of the string
|
||||
"""
|
||||
|
||||
if size <= 0:
|
||||
return "0 B" if unit else "0"
|
||||
|
||||
units = ['B','KiB','MiB','GiB','TiB','PiB','EiB','ZiB','YiB']
|
||||
p = math.floor(math.log(size, 2) / 10)
|
||||
|
||||
if unit:
|
||||
return "%.3f %s" % (size / math.pow(1024,p), units[int(p)])
|
||||
else:
|
||||
return "%.3f" % (size / math.pow(1024,p))
|
||||
175
nemubot/tools/web.py
Normal file
175
nemubot/tools/web.py
Normal file
|
|
@ -0,0 +1,175 @@
|
|||
# coding=utf-8
|
||||
|
||||
# Nemubot is a modulable IRC bot, built around XML configuration files.
|
||||
# Copyright (C) 2012 Mercier Pierre-Olivier
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from html.entities import name2codepoint
|
||||
import http.client
|
||||
import json
|
||||
import re
|
||||
import socket
|
||||
from urllib.parse import quote
|
||||
from urllib.parse import urlparse
|
||||
from urllib.request import urlopen
|
||||
|
||||
from bot import __version__
|
||||
from exception import IRCException
|
||||
from tools.xmlparser import parse_string
|
||||
|
||||
|
||||
def isURL(url):
|
||||
"""Return True if the URL can be parsed"""
|
||||
o = urlparse(url)
|
||||
return o.scheme == "" and o.netloc == "" and o.path == ""
|
||||
|
||||
|
||||
def getScheme(url):
|
||||
"""Return the protocol of a given URL"""
|
||||
o = urlparse(url)
|
||||
return o.scheme
|
||||
|
||||
|
||||
def getHost(url):
|
||||
"""Return the domain of a given URL"""
|
||||
return urlparse(url).hostname
|
||||
|
||||
|
||||
def getPort(url):
|
||||
"""Return the port of a given URL"""
|
||||
return urlparse(url).port
|
||||
|
||||
|
||||
def getPath(url):
|
||||
"""Return the page request of a given URL"""
|
||||
return urlparse(url).path
|
||||
|
||||
|
||||
def getUser(url):
|
||||
"""Return the page request of a given URL"""
|
||||
return urlparse(url).username
|
||||
|
||||
|
||||
def getPassword(url):
|
||||
"""Return the page request of a given URL"""
|
||||
return urlparse(url).password
|
||||
|
||||
|
||||
# Get real pages
|
||||
|
||||
def getURLContent(url, timeout=15):
|
||||
"""Return page content corresponding to URL or None if any error occurs"""
|
||||
o = urlparse(url)
|
||||
if o.netloc == "":
|
||||
o = urlparse("http://" + url)
|
||||
|
||||
if o.scheme == "http":
|
||||
conn = http.client.HTTPConnection(o.hostname, port=o.port,
|
||||
timeout=timeout)
|
||||
elif o.scheme == "https":
|
||||
conn = http.client.HTTPSConnection(o.hostname, port=o.port,
|
||||
timeout=timeout)
|
||||
elif o.scheme is None or o.scheme == "":
|
||||
conn = http.client.HTTPConnection(o.hostname, port=80, timeout=timeout)
|
||||
else:
|
||||
return None
|
||||
try:
|
||||
if o.query != '':
|
||||
conn.request("GET", o.path + "?" + o.query,
|
||||
None, {"User-agent": "Nemubot v%s" % __version__})
|
||||
else:
|
||||
conn.request("GET", o.path, None, {"User-agent":
|
||||
"Nemubot v%s" % __version__})
|
||||
except socket.timeout:
|
||||
return None
|
||||
except OSError: # [Errno 113] No route to host
|
||||
return None
|
||||
except socket.gaierror:
|
||||
print ("<tools.web> Unable to receive page %s on %s from %s."
|
||||
% (o.path, o.netloc, url))
|
||||
return None
|
||||
|
||||
try:
|
||||
res = conn.getresponse()
|
||||
size = int(res.getheader("Content-Length", 524288))
|
||||
cntype = res.getheader("Content-Type")
|
||||
|
||||
if size > 524288 or (cntype is not None and cntype[:4] != "text" and cntype[:4] != "appl"):
|
||||
return None
|
||||
|
||||
data = res.read(size)
|
||||
|
||||
# Decode content
|
||||
charset = "utf-8"
|
||||
if cntype is not None:
|
||||
lcharset = res.getheader("Content-Type").split(";")
|
||||
if len(lcharset) > 1:
|
||||
for c in charset:
|
||||
ch = c.split("=")
|
||||
if ch[0].strip().lower() == "charset" and len(ch) > 1:
|
||||
cha = ch[1].split(".")
|
||||
if len(cha) > 1:
|
||||
charset = cha[1]
|
||||
else:
|
||||
charset = cha[0]
|
||||
except http.client.BadStatusLine:
|
||||
raise IRCException("Invalid HTTP response")
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
if res.status == http.client.OK or res.status == http.client.SEE_OTHER:
|
||||
return data.decode(charset)
|
||||
elif ((res.status == http.client.FOUND or
|
||||
res.status == http.client.MOVED_PERMANENTLY) and
|
||||
res.getheader("Location") != url):
|
||||
return getURLContent(res.getheader("Location"), timeout)
|
||||
else:
|
||||
raise IRCException("A HTTP error occurs: %d - %s" %
|
||||
(res.status, http.client.responses[res.status]))
|
||||
|
||||
|
||||
def getXML(url, timeout=15):
|
||||
"""Get content page and return XML parsed content"""
|
||||
cnt = getURLContent(url, timeout)
|
||||
if cnt is None:
|
||||
return None
|
||||
else:
|
||||
return parse_string(cnt.encode())
|
||||
|
||||
|
||||
def getJSON(url, timeout=15):
|
||||
"""Get content page and return JSON content"""
|
||||
cnt = getURLContent(url, timeout)
|
||||
if cnt is None:
|
||||
return None
|
||||
else:
|
||||
return json.loads(cnt)
|
||||
|
||||
|
||||
# Other utils
|
||||
|
||||
def htmlentitydecode(s):
|
||||
"""Decode htmlentities"""
|
||||
return re.sub('&(%s);' % '|'.join(name2codepoint),
|
||||
lambda m: chr(name2codepoint[m.group(1)]), s)
|
||||
|
||||
|
||||
def striphtml(data):
|
||||
"""Remove HTML tags from text"""
|
||||
p = re.compile(r'<.*?>')
|
||||
return htmlentitydecode(p.sub('', data)
|
||||
.replace("(", "/(")
|
||||
.replace(")", ")/")
|
||||
.replace(""", "\""))
|
||||
64
nemubot/tools/xmlparser/__init__.py
Normal file
64
nemubot/tools/xmlparser/__init__.py
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Nemubot is a modulable IRC bot, built around XML configuration files.
|
||||
# Copyright (C) 2012 Mercier Pierre-Olivier
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import logging
|
||||
import xml.sax
|
||||
|
||||
from . import node as module_state
|
||||
|
||||
logger = logging.getLogger("nemubot.tools.xmlparser")
|
||||
|
||||
|
||||
class ModuleStatesFile(xml.sax.ContentHandler):
|
||||
def startDocument(self):
|
||||
self.root = None
|
||||
self.stack = list()
|
||||
|
||||
def startElement(self, name, attrs):
|
||||
cur = module_state.ModuleState(name)
|
||||
|
||||
for name in attrs.keys():
|
||||
cur.setAttribute(name, attrs.getValue(name))
|
||||
|
||||
self.stack.append(cur)
|
||||
|
||||
def characters(self, content):
|
||||
self.stack[len(self.stack)-1].content += content
|
||||
|
||||
def endElement(self, name):
|
||||
child = self.stack.pop()
|
||||
size = len(self.stack)
|
||||
if size > 0:
|
||||
self.stack[size - 1].content = self.stack[size - 1].content.strip()
|
||||
self.stack[size - 1].addChild(child)
|
||||
else:
|
||||
self.root = child
|
||||
|
||||
|
||||
def parse_file(filename):
|
||||
parser = xml.sax.make_parser()
|
||||
mod = ModuleStatesFile()
|
||||
parser.setContentHandler(mod)
|
||||
parser.parse(open(filename, "r"))
|
||||
return mod.root
|
||||
|
||||
|
||||
def parse_string(string):
|
||||
mod = ModuleStatesFile()
|
||||
xml.sax.parseString(string, mod)
|
||||
return mod.root
|
||||
212
nemubot/tools/xmlparser/node.py
Normal file
212
nemubot/tools/xmlparser/node.py
Normal file
|
|
@ -0,0 +1,212 @@
|
|||
# coding=utf-8
|
||||
|
||||
import xml.sax
|
||||
from datetime import datetime, timezone
|
||||
import logging
|
||||
import time
|
||||
|
||||
logger = logging.getLogger("nemubot.tools.xmlparser.node")
|
||||
|
||||
|
||||
class ModuleState:
|
||||
"""Tiny tree representation of an XML file"""
|
||||
|
||||
def __init__(self, name):
|
||||
self.name = name
|
||||
self.content = ""
|
||||
self.attributes = dict()
|
||||
self.childs = list()
|
||||
self.index = dict()
|
||||
self.index_fieldname = None
|
||||
self.index_tagname = None
|
||||
|
||||
def getName(self):
|
||||
"""Get the name of the current node"""
|
||||
return self.name
|
||||
|
||||
def display(self, level = 0):
|
||||
ret = ""
|
||||
out = list()
|
||||
for k in self.attributes:
|
||||
out.append("%s : %s" % (k, self.attributes[k]))
|
||||
ret += "%s%s { %s } = '%s'\n" % (' ' * level, self.name,
|
||||
' ; '.join(out), self.content)
|
||||
for c in self.childs:
|
||||
ret += c.display(level + 2)
|
||||
return ret
|
||||
|
||||
def __str__(self):
|
||||
return self.display()
|
||||
|
||||
def __getitem__(self, i):
|
||||
"""Return the attribute asked"""
|
||||
return self.getAttribute(i)
|
||||
|
||||
def __setitem__(self, i, c):
|
||||
"""Set the attribute"""
|
||||
return self.setAttribute(i, c)
|
||||
|
||||
def getAttribute(self, name):
|
||||
"""Get the asked argument or return None if doesn't exist"""
|
||||
if name in self.attributes:
|
||||
return self.attributes[name]
|
||||
else:
|
||||
return None
|
||||
|
||||
def getDate(self, name=None):
|
||||
"""Get the asked argument and return it as a date"""
|
||||
if name is None:
|
||||
source = self.content
|
||||
elif name in self.attributes.keys():
|
||||
source = self.attributes[name]
|
||||
else:
|
||||
return None
|
||||
|
||||
if isinstance(source, datetime):
|
||||
return source
|
||||
else:
|
||||
try:
|
||||
return datetime.fromtimestamp(float(source), timezone.utc)
|
||||
except ValueError:
|
||||
while True:
|
||||
try:
|
||||
return datetime.fromtimestamp(time.mktime(
|
||||
time.strptime(source[:19], "%Y-%m-%d %H:%M:%S")),
|
||||
timezone.utc)
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
def getInt(self, name=None):
|
||||
"""Get the asked argument and return it as an integer"""
|
||||
if name is None:
|
||||
source = self.content
|
||||
elif name in self.attributes.keys():
|
||||
source = self.attributes[name]
|
||||
else:
|
||||
return None
|
||||
|
||||
return int(float(source))
|
||||
|
||||
def getBool(self, name=None):
|
||||
"""Get the asked argument and return it as an integer"""
|
||||
if name is None:
|
||||
source = self.content
|
||||
elif name in self.attributes.keys():
|
||||
source = self.attributes[name]
|
||||
else:
|
||||
return False
|
||||
|
||||
return (isinstance(source, bool) and source) or source == "True"
|
||||
|
||||
def tmpIndex(self, fieldname="name", tagname=None):
|
||||
index = dict()
|
||||
for child in self.childs:
|
||||
if ((tagname is None or tagname == child.name) and
|
||||
child.hasAttribute(fieldname)):
|
||||
index[child[fieldname]] = child
|
||||
return index
|
||||
|
||||
def setIndex(self, fieldname="name", tagname=None):
|
||||
"""Defines an hash table to accelerate childs search.
|
||||
You have just to define a common attribute"""
|
||||
self.index = self.tmpIndex(fieldname, tagname)
|
||||
self.index_fieldname = fieldname
|
||||
self.index_tagname = tagname
|
||||
|
||||
def __contains__(self, i):
|
||||
"""Return true if i is found in the index"""
|
||||
if self.index:
|
||||
return i in self.index
|
||||
else:
|
||||
return self.hasAttribute(i)
|
||||
|
||||
def hasAttribute(self, name):
|
||||
"""DOM like method"""
|
||||
return (name in self.attributes)
|
||||
|
||||
def setAttribute(self, name, value):
|
||||
"""DOM like method"""
|
||||
if (isinstance(value, datetime) or isinstance(value, str) or
|
||||
isinstance(value, int) or isinstance(value, float)):
|
||||
self.attributes[name] = value
|
||||
else:
|
||||
raise TypeError("attributes must be primary type "
|
||||
"or datetime (here %s)" % type(value))
|
||||
|
||||
def getContent(self):
|
||||
return self.content
|
||||
|
||||
def getChilds(self):
|
||||
"""Return a full list of direct child of this node"""
|
||||
return self.childs
|
||||
|
||||
def getNode(self, tagname):
|
||||
"""Get a unique node (or the last one) with the given tagname"""
|
||||
ret = None
|
||||
for child in self.childs:
|
||||
if tagname is None or tagname == child.name:
|
||||
ret = child
|
||||
return ret
|
||||
|
||||
def getFirstNode(self, tagname):
|
||||
"""Get a unique node (or the last one) with the given tagname"""
|
||||
for child in self.childs:
|
||||
if tagname is None or tagname == child.name:
|
||||
return child
|
||||
return None
|
||||
|
||||
def getNodes(self, tagname):
|
||||
"""Get all direct childs that have the given tagname"""
|
||||
for child in self.childs:
|
||||
if tagname is None or tagname == child.name:
|
||||
yield child
|
||||
|
||||
def hasNode(self, tagname):
|
||||
"""Return True if at least one node with the given tagname exists"""
|
||||
for child in self.childs:
|
||||
if tagname is None or tagname == child.name:
|
||||
return True
|
||||
return False
|
||||
|
||||
def addChild(self, child):
|
||||
"""Add a child to this node"""
|
||||
self.childs.append(child)
|
||||
if self.index_fieldname is not None:
|
||||
self.setIndex(self.index_fieldname, self.index_tagname)
|
||||
|
||||
def delChild(self, child):
|
||||
"""Remove the given child from this node"""
|
||||
self.childs.remove(child)
|
||||
if self.index_fieldname is not None:
|
||||
self.setIndex(self.index_fieldname, self.index_tagname)
|
||||
|
||||
def save_node(self, gen):
|
||||
"""Serialize this node as a XML node"""
|
||||
attribs = {}
|
||||
for att in self.attributes.keys():
|
||||
if att[0] != "_": # Don't save attribute starting by _
|
||||
if isinstance(self.attributes[att], datetime):
|
||||
attribs[att] = str(time.mktime(
|
||||
self.attributes[att].timetuple()))
|
||||
else:
|
||||
attribs[att] = str(self.attributes[att])
|
||||
attrs = xml.sax.xmlreader.AttributesImpl(attribs)
|
||||
|
||||
try:
|
||||
gen.startElement(self.name, attrs)
|
||||
|
||||
for child in self.childs:
|
||||
child.save_node(gen)
|
||||
|
||||
gen.endElement(self.name)
|
||||
except:
|
||||
logger.exception("Error occured when saving the following "
|
||||
"XML node: %s with %s", self.name, attrs)
|
||||
|
||||
def save(self, filename):
|
||||
"""Save the current node as root node in a XML file"""
|
||||
with open(filename, "w") as f:
|
||||
gen = xml.sax.saxutils.XMLGenerator(f, "utf-8")
|
||||
gen.startDocument()
|
||||
self.save_node(gen)
|
||||
gen.endDocument()
|
||||
Loading…
Add table
Add a link
Reference in a new issue