mirror of
https://github.com/Thor77/TeamspeakStats.git
synced 2025-04-13 00:45:33 -04:00
Compare commits
319 commits
Author | SHA1 | Date | |
---|---|---|---|
|
6e069659b7 | ||
|
4c872930db | ||
|
1a7c1cce9d | ||
|
dcc26830cb | ||
|
7ae09263e1 | ||
|
1d0e3a5fdf | ||
|
51abb87c88 | ||
|
43e759c714 | ||
|
8ff880645a | ||
|
4a22a06a91 | ||
|
4d09f9d82c | ||
|
60f102cc34 | ||
|
8223c389d3 | ||
|
0a443467be | ||
|
561f1b6e4d | ||
|
e90e3fad43 | ||
|
61c6022e37 | ||
|
f6c5be663a | ||
|
a9c942d442 | ||
|
127370adbd | ||
|
b327c4e43d | ||
|
40874d525b | ||
|
c7f9731b6a | ||
|
604dc0a439 | ||
|
b8dbd442f2 | ||
|
18f1576bfb | ||
|
7f88f0376c | ||
|
1f177ff49b | ||
|
544da4062f | ||
|
35c5c91fb0 | ||
|
21f9f6581b | ||
|
371af752eb | ||
|
b81b1bca10 | ||
|
c34f2d4d8b | ||
|
3207c0070c | ||
|
4069ab659a | ||
|
0dc12e73a1 | ||
|
b474e4124d | ||
|
2344915d6e | ||
|
3ce4962e12 | ||
|
dc572ad8fa | ||
|
c14e7f1068 | ||
|
687f58e247 | ||
|
f878fefeaa | ||
|
187ae0afc5 | ||
|
a019278e79 | ||
|
e92f5ea5ff | ||
|
1ef4a3bc15 | ||
|
6e40555612 | ||
|
5d9507deb0 | ||
|
ae13390b7b | ||
|
5ea2f6ab3d | ||
|
489d609807 | ||
|
56471137c0 | ||
|
4310f93adc | ||
|
ab68f57f83 | ||
|
7fd4297c4d | ||
|
8d1c19a734 | ||
|
2ebd445349 | ||
|
c9ab6f6b97 | ||
|
c8092018f2 | ||
|
51191672c6 | ||
|
871210dde4 | ||
|
1e1f112867 | ||
|
a9e8cd0b6e | ||
|
8558d731d4 | ||
|
f786c87dfb | ||
|
52f5cc3ac1 | ||
|
b9f798d04d | ||
|
6345c3f1f5 | ||
|
b1b80f657a | ||
|
cbc76b5541 | ||
|
1c224fa0ee | ||
|
c79dd08bc0 | ||
|
147c41ffce | ||
|
df268f1c2a | ||
|
602e6c4d51 | ||
|
91a9b8e4c7 | ||
|
90a367da27 | ||
|
59d4c88701 | ||
|
3d6c41538b | ||
|
96d6e9f050 | ||
|
7077446627 | ||
|
3acf282470 | ||
|
c2fb6aa6c1 | ||
|
088d905196 | ||
|
a084101ced | ||
|
da2b773bf6 | ||
|
08b4e06f10 | ||
|
20d40c8890 | ||
|
caff246f9a | ||
|
6a84b35a52 | ||
|
7ab4436777 | ||
|
3067c229d6 | ||
|
7dbd37b028 | ||
|
22c15202a3 | ||
|
edff1e956d | ||
|
65a8379261 | ||
|
3d469ce28c | ||
|
6ddc8c94b7 | ||
|
de1cc4be2f | ||
|
ed62eceda3 | ||
|
11acf9f9b6 | ||
|
992d35ec87 | ||
|
f209573d04 | ||
|
a4c04e34c8 | ||
|
fec833d876 | ||
|
8299c73eb9 | ||
|
e37e292ba9 | ||
|
cbf7000e91 | ||
|
ef5894b407 | ||
|
97d58254b3 | ||
|
8d18b9c4ae | ||
|
4b261ed321 | ||
|
1bc555d66c | ||
|
683f9b984a | ||
|
6e662e2555 | ||
|
270b20d385 | ||
|
0081ac9939 | ||
|
e4212f28fb | ||
|
a83d6de253 | ||
|
cb42b9ee2a | ||
|
bdba91879e | ||
|
aaf5f2b851 | ||
|
a694a2bc58 | ||
|
6238f14574 | ||
|
cd01eb433a | ||
|
a1274a52f4 | ||
|
cf82835b46 | ||
|
c665babc94 | ||
|
fb5a136b9d | ||
|
6c35aed767 | ||
|
8f49c3e95d | ||
|
0033ce186f | ||
|
2753f548fe | ||
|
962fd486af | ||
|
709c573b65 | ||
|
5e19e38965 | ||
|
f68986117b | ||
|
f893c42b31 | ||
|
5968dc31dd | ||
|
2816c2ecfa | ||
|
5db1345717 | ||
|
4adfb9cfc1 | ||
|
531f5c57d2 | ||
|
ba8b393b76 | ||
|
9d5547ccfd | ||
|
a47c7a6728 | ||
|
f0c33b2ad9 | ||
|
d5c3f312df | ||
|
ab96dd42f9 | ||
|
3824eeac10 | ||
|
69557a94a6 | ||
|
ae35a73a64 | ||
|
d7876f25b6 | ||
|
96156ca622 | ||
|
e94e117242 | ||
|
13ad296459 | ||
|
d75779a9f2 | ||
|
72c1eb78f8 | ||
|
fe0965db32 | ||
|
426c0199b4 | ||
|
e005fdeb92 | ||
|
cc0f1b30f7 | ||
|
22f6402bce | ||
|
6366d3ebb1 | ||
|
f97f309b85 | ||
|
38d497300c | ||
|
365994500a | ||
|
8d2e0c1345 | ||
|
7a55829d0b | ||
|
cb449558e0 | ||
|
42fb5f30b5 | ||
|
17a486bb7d | ||
|
e7e8612bc0 | ||
|
ba47122f50 | ||
|
ef66b45e21 | ||
|
2cdcdd1e5b | ||
|
a96200dafa | ||
|
a3d4cdde5b | ||
|
418be10603 | ||
|
42796be9d8 | ||
|
5915664605 | ||
|
062da49244 | ||
|
9a686f7a2f | ||
|
4f77b2b6f3 | ||
|
51225175ce | ||
|
cbbedfa669 | ||
|
75c5ebb575 | ||
|
f0dc95a583 | ||
|
fdbde18856 | ||
|
6003543309 | ||
|
117dff2486 | ||
|
87e5d697cb | ||
|
9c09f34f02 | ||
|
273a01bec6 | ||
|
9a38e0e7cb | ||
|
4bed26dac8 | ||
|
b8d2df4650 | ||
|
3acb1af132 | ||
|
5b2ad90436 | ||
|
2e234d00bb | ||
|
5dc21e89de | ||
|
d522ce648a | ||
|
5637a11b2b | ||
|
4ac27143a6 | ||
|
17392494ed | ||
|
414054c243 | ||
|
15a437ea2c | ||
|
338f6cd6de | ||
|
1ec0a37480 | ||
|
db24696c47 | ||
|
03c0941962 | ||
|
c8955bee5e | ||
|
679473f7b4 | ||
|
812916f179 | ||
|
6c301c2ed4 | ||
|
ba8e082e64 | ||
|
edc914b451 | ||
|
7cd057d3c5 | ||
|
c0096a7c52 | ||
|
1c4ee4d72d | ||
|
de7afd87e7 | ||
|
419164f332 | ||
|
96ffef1b5a | ||
|
8020e047f6 | ||
|
b520fe588f | ||
|
4c89eb8302 | ||
|
e5be93b17a | ||
|
a1f535961e | ||
|
1a485c67c5 | ||
|
de5f58cb93 | ||
|
37b8b7bc26 | ||
|
76948713a4 | ||
|
2d80ba6804 | ||
|
c0523717a7 | ||
|
b6e73e733d | ||
|
80df2c02f0 | ||
|
9c3c772db9 | ||
|
1b1ed86750 | ||
|
040b451c7d | ||
|
c59d182da4 | ||
|
9682e2fe6a | ||
|
45f9ba2737 | ||
|
f7669792c1 | ||
|
3a1d51a60b | ||
|
d1627d369f | ||
|
959423be81 | ||
|
ddd220d629 | ||
|
08d282b6ca | ||
|
799f622201 | ||
|
52fc1b487c | ||
|
4078e4b06b | ||
|
1ecf24b9b2 | ||
|
bd2157c2b8 | ||
|
5c52ab1995 | ||
|
bbd2ff7a46 | ||
|
604fefe286 | ||
|
07b61d86ce | ||
|
3f140b8d6a | ||
|
a475caa7c7 | ||
|
5ff08e0163 | ||
|
cfb593ba9c | ||
|
87f9bf43fc | ||
|
708f071033 | ||
|
2a1ab472bb | ||
|
4034410e2d | ||
|
67c08330d8 | ||
|
402040e2dc | ||
|
60bdcb7aab | ||
|
477ca7d739 | ||
|
ea64ee0ecb | ||
|
6638eaa044 | ||
|
c25b51dd90 | ||
|
6c33524850 | ||
|
34e682cf81 | ||
|
bb808ca8c0 | ||
|
892680fe4d | ||
|
573838c35e | ||
|
487a50508f | ||
|
95c22dde35 | ||
|
eafc98f548 | ||
|
0b667f55b7 | ||
|
def9f2e1e2 | ||
|
21be54675f | ||
|
54532fd598 | ||
|
468bfcd22d | ||
|
c132c17661 | ||
|
c0f1a6c649 | ||
|
9a6bbe4f3e | ||
|
976d40e2b9 | ||
|
105f464b9a | ||
|
e92ad9e6fe | ||
|
0aa0c7b7ea | ||
|
612055a088 | ||
|
0c57b27abc | ||
|
37a9841900 | ||
|
ad7ff96b1b | ||
|
bec0279871 | ||
|
2821713150 | ||
|
4c694a7770 | ||
|
8abb0029fa | ||
|
850d3463ce | ||
|
123370554a | ||
|
95c516a9f3 | ||
|
12a86539d2 | ||
|
e3df7f8185 | ||
|
2ed3b7f48d | ||
|
32234b4886 | ||
|
c3dabb9082 | ||
|
28855e9a81 | ||
|
89906d04c7 | ||
|
9d5197d813 | ||
|
3fec44feaa | ||
|
276dce0074 | ||
|
17d7552e8b | ||
|
4e4eacd3af | ||
|
a1c5f67c0c | ||
|
3972594787 |
50 changed files with 1826 additions and 731 deletions
.coveragerc
.github/workflows
.gitignore.travis.ymlLICENSEREADME.mdREADME.rstdocs
poetry.lockpyproject.tomlrequirements.txtscreenshot.pngsetup.pytesting_requirements.txttsstats
|
@ -8,3 +8,4 @@ exclude_lines =
|
|||
omit =
|
||||
tsstats/tests/*
|
||||
tsstats/__main__.py
|
||||
tsstats/logger.py
|
||||
|
|
23
.github/workflows/publish.yml
vendored
Normal file
23
.github/workflows/publish.yml
vendored
Normal file
|
@ -0,0 +1,23 @@
|
|||
name: publish
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "*"
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.10'
|
||||
- name: Setup poetry
|
||||
uses: abatilo/actions-poetry@v2
|
||||
with:
|
||||
poetry-version: '1.3'
|
||||
- name: Publish
|
||||
run: poetry publish --build
|
||||
env:
|
||||
POETRY_PYPI_TOKEN_PYPI : ${{ secrets.POETRY_PYPI_TOKEN_PYPI }}
|
42
.github/workflows/test.yml
vendored
Normal file
42
.github/workflows/test.yml
vendored
Normal file
|
@ -0,0 +1,42 @@
|
|||
name: test
|
||||
|
||||
on:
|
||||
push: {}
|
||||
pull_request: {}
|
||||
workflow_call: {}
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.10'
|
||||
- name: Setup poetry
|
||||
uses: abatilo/actions-poetry@v2
|
||||
with:
|
||||
poetry-version: '1.3'
|
||||
- name: Install dependencies
|
||||
run: poetry install
|
||||
- name: Lint
|
||||
run: poetry run poe lint
|
||||
test:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
python-version: ["3.7", "3.8", "3.9", "3.10"]
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- name: Setup poetry
|
||||
uses: abatilo/actions-poetry@v2
|
||||
with:
|
||||
poetry-version: '1.3'
|
||||
- name: Install dependencies
|
||||
run: poetry install
|
||||
- name: Run tests
|
||||
run: poetry run poe test
|
11
.gitignore
vendored
11
.gitignore
vendored
|
@ -1,2 +1,11 @@
|
|||
*.pyc
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
venv/
|
||||
.cache/
|
||||
|
||||
build/
|
||||
dist/
|
||||
*.egg-info/
|
||||
*.egg
|
||||
|
||||
docs/_build
|
||||
|
|
25
.travis.yml
25
.travis.yml
|
@ -1,25 +0,0 @@
|
|||
language: python
|
||||
python:
|
||||
- 2.7
|
||||
- 3.5
|
||||
|
||||
matrix:
|
||||
include:
|
||||
- python: 2.7
|
||||
install:
|
||||
- pip install pyflakes
|
||||
- pip install isort
|
||||
script:
|
||||
- pyflakes tsstats/**/*.py
|
||||
- isort -c tsstats/**/*.py
|
||||
|
||||
install:
|
||||
- pip install -r requirements.txt
|
||||
- pip install -r testing_requirements.txt
|
||||
- pip install pytest-cov
|
||||
|
||||
script: py.test --cov=tsstats tsstats/
|
||||
|
||||
after_success:
|
||||
- pip install coveralls
|
||||
- coveralls
|
3
LICENSE
3
LICENSE
|
@ -1,6 +1,6 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015
|
||||
Copyright (c) 2017 Thor77
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
@ -19,4 +19,3 @@ 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.
|
||||
|
||||
|
|
65
README.md
65
README.md
|
@ -1,65 +0,0 @@
|
|||
# TeamspeakStats [](https://travis-ci.org/Thor77/TeamspeakStats) [](https://coveralls.io/github/Thor77/TeamspeakStats?branch=master) [](https://landscape.io/github/Thor77/TeamspeakStats/master) [](https://pypi.python.org/pypi/tsstats) [](http://teamspeakstats.readthedocs.io/en/latest/?badge=latest)
|
||||
A simple Teamspeak stat-generator - based on server-logs
|
||||
|
||||

|
||||
|
||||
# Installation
|
||||
- Install the package via PyPi `pip install tsstats`
|
||||
|
||||
# Usage
|
||||
- Create a config (see [Configuration](https://github.com/Thor77/TeamspeakStats#configuration))
|
||||
- Run the script `tsstats [-h]`
|
||||
|
||||
# Tests
|
||||
- Install testing-requirements `pip install -r testing_requirements.txt`
|
||||
- Run `py.test tsstats/`
|
||||
|
||||
# CMD-Arguments
|
||||
```
|
||||
usage: tsstats [-h] [-c CONFIG] [--idmap IDMAP] [-l LOG] [-o OUTPUT] [-d]
|
||||
|
||||
A simple Teamspeak stats-generator - based on server-logs
|
||||
|
||||
optional arguments:
|
||||
-h, --help show this help message and exit
|
||||
-c CONFIG, --config CONFIG
|
||||
path to config
|
||||
--idmap IDMAP path to id_map
|
||||
-l LOG, --log LOG path to your logfile(s)
|
||||
-o OUTPUT, --output OUTPUT
|
||||
path to the output-file
|
||||
-d, --debug debug mode
|
||||
```
|
||||
|
||||
# Configuration
|
||||
|
||||
#### [General]
|
||||
| Key | Description |
|
||||
|-----|-------------|
|
||||
| log | Path to TS3Server-logfile(s) (supports [globbing](https://docs.python.org/3/library/glob.html)) |
|
||||
| output | Path to the location, where the generator will put the generated `.html`-file |
|
||||
| idmap | Path to [IdentMap](http://teamspeakstats.readthedocs.io/en/latest/identmap.html) |
|
||||
| debug | debug mode |
|
||||
|
||||
|
||||
## Example
|
||||
```
|
||||
[General]
|
||||
log = /usr/local/bin/teamspeak-server/logs/ts3server*_1.log
|
||||
output = /var/www/html/stats.html
|
||||
```
|
||||
|
||||
# ID-Mapping
|
||||
`id_map.json`
|
||||
You can map multiple ID's to one (for example, when an user creates a new identity)
|
||||
## Example
|
||||
```json
|
||||
{
|
||||
"1": "2",
|
||||
"3": "2"
|
||||
}
|
||||
```
|
||||
The online-time of `1` and `3` will be added to the online-time of `2`
|
||||
|
||||
# TODO
|
||||
- Localization
|
47
README.rst
Normal file
47
README.rst
Normal file
|
@ -0,0 +1,47 @@
|
|||
TeamspeakStats |Build Status| |Build status| |Coverage Status| |PyPI| |Documentation Status|
|
||||
==========================================================================================================
|
||||
|
||||
A simple Teamspeak stat-generator - based solely on server-logs
|
||||
|
||||
|screenshot|
|
||||
|
||||
Installation
|
||||
============
|
||||
|
||||
- Install the package via PyPi ``pip install tsstats``
|
||||
- Clone this repo
|
||||
``git clone https://github.com/Thor77/TeamspeakStats`` and install
|
||||
with ``python setup.py install``
|
||||
- Just use the package as is via ``python -m tsstats [-h]``
|
||||
|
||||
Usage
|
||||
=====
|
||||
|
||||
- Run the script ``tsstats [-h]``
|
||||
- Optionally create a config-file (see
|
||||
`Configuration <https://teamspeakstats.readthedocs.io/en/latest/config.html>`__)
|
||||
- The package works entirely off your Teamspeak server's logs, so that
|
||||
no ServerQuery account is necessary
|
||||
|
||||
Example
|
||||
=======
|
||||
|
||||
::
|
||||
|
||||
tsstats -l /var/log/teamspeak3-server/ -o /var/www/tsstats.html
|
||||
|
||||
Parse logs in ``/var/log/teamspeak3-server`` and write output to ``/var/www/tsstats.html``.
|
||||
|
||||
For more details checkout the `documentation <http://teamspeakstats.readthedocs.io/en/latest/>`__!
|
||||
|
||||
.. |screenshot| image:: https://raw.githubusercontent.com/Thor77/TeamspeakStats/master/screenshot.png
|
||||
.. |Build Status| image:: https://travis-ci.org/Thor77/TeamspeakStats.svg?branch=master
|
||||
:target: https://travis-ci.org/Thor77/TeamspeakStats
|
||||
.. |Build status| image:: https://ci.appveyor.com/api/projects/status/u9cx7krwmmevbvl2/branch/master?svg=true
|
||||
:target: https://ci.appveyor.com/project/Thor77/teamspeakstats
|
||||
.. |Coverage Status| image:: https://coveralls.io/repos/Thor77/TeamspeakStats/badge.svg?branch=master&service=github
|
||||
:target: https://coveralls.io/github/Thor77/TeamspeakStats?branch=master
|
||||
.. |PyPI| image:: https://img.shields.io/pypi/v/tsstats.svg
|
||||
:target: https://pypi.python.org/pypi/tsstats
|
||||
.. |Documentation Status| image:: https://readthedocs.org/projects/teamspeakstats/badge/?version=latest
|
||||
:target: http://teamspeakstats.readthedocs.io/en/latest/?badge=latest
|
194
docs/Makefile
194
docs/Makefile
|
@ -1,192 +1,20 @@
|
|||
# Makefile for Sphinx documentation
|
||||
# Minimal makefile for Sphinx documentation
|
||||
#
|
||||
|
||||
# You can set these variables from the command line.
|
||||
SPHINXOPTS =
|
||||
SPHINXBUILD = sphinx-build
|
||||
PAPER =
|
||||
BUILDDIR = build
|
||||
|
||||
# User-friendly check for sphinx-build
|
||||
ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1)
|
||||
$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/)
|
||||
endif
|
||||
|
||||
# Internal variables.
|
||||
PAPEROPT_a4 = -D latex_paper_size=a4
|
||||
PAPEROPT_letter = -D latex_paper_size=letter
|
||||
ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source
|
||||
# the i18n builder cannot share the environment and doctrees with the others
|
||||
I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source
|
||||
|
||||
.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest coverage gettext
|
||||
SPHINXPROJ = TeamspeakStats
|
||||
SOURCEDIR = .
|
||||
BUILDDIR = _build
|
||||
|
||||
# Put it first so that "make" without argument is like "make help".
|
||||
help:
|
||||
@echo "Please use \`make <target>' where <target> is one of"
|
||||
@echo " html to make standalone HTML files"
|
||||
@echo " dirhtml to make HTML files named index.html in directories"
|
||||
@echo " singlehtml to make a single large HTML file"
|
||||
@echo " pickle to make pickle files"
|
||||
@echo " json to make JSON files"
|
||||
@echo " htmlhelp to make HTML files and a HTML help project"
|
||||
@echo " qthelp to make HTML files and a qthelp project"
|
||||
@echo " applehelp to make an Apple Help Book"
|
||||
@echo " devhelp to make HTML files and a Devhelp project"
|
||||
@echo " epub to make an epub"
|
||||
@echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
|
||||
@echo " latexpdf to make LaTeX files and run them through pdflatex"
|
||||
@echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx"
|
||||
@echo " text to make text files"
|
||||
@echo " man to make manual pages"
|
||||
@echo " texinfo to make Texinfo files"
|
||||
@echo " info to make Texinfo files and run them through makeinfo"
|
||||
@echo " gettext to make PO message catalogs"
|
||||
@echo " changes to make an overview of all changed/added/deprecated items"
|
||||
@echo " xml to make Docutils-native XML files"
|
||||
@echo " pseudoxml to make pseudoxml-XML files for display purposes"
|
||||
@echo " linkcheck to check all external links for integrity"
|
||||
@echo " doctest to run all doctests embedded in the documentation (if enabled)"
|
||||
@echo " coverage to run coverage check of the documentation (if enabled)"
|
||||
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
||||
|
||||
clean:
|
||||
rm -rf $(BUILDDIR)/*
|
||||
.PHONY: help Makefile
|
||||
|
||||
html:
|
||||
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
|
||||
@echo
|
||||
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
|
||||
|
||||
dirhtml:
|
||||
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
|
||||
@echo
|
||||
@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
|
||||
|
||||
singlehtml:
|
||||
$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
|
||||
@echo
|
||||
@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
|
||||
|
||||
pickle:
|
||||
$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
|
||||
@echo
|
||||
@echo "Build finished; now you can process the pickle files."
|
||||
|
||||
json:
|
||||
$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
|
||||
@echo
|
||||
@echo "Build finished; now you can process the JSON files."
|
||||
|
||||
htmlhelp:
|
||||
$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
|
||||
@echo
|
||||
@echo "Build finished; now you can run HTML Help Workshop with the" \
|
||||
".hhp project file in $(BUILDDIR)/htmlhelp."
|
||||
|
||||
qthelp:
|
||||
$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
|
||||
@echo
|
||||
@echo "Build finished; now you can run "qcollectiongenerator" with the" \
|
||||
".qhcp project file in $(BUILDDIR)/qthelp, like this:"
|
||||
@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/TeamspeakStats.qhcp"
|
||||
@echo "To view the help file:"
|
||||
@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/TeamspeakStats.qhc"
|
||||
|
||||
applehelp:
|
||||
$(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp
|
||||
@echo
|
||||
@echo "Build finished. The help book is in $(BUILDDIR)/applehelp."
|
||||
@echo "N.B. You won't be able to view it unless you put it in" \
|
||||
"~/Library/Documentation/Help or install it in your application" \
|
||||
"bundle."
|
||||
|
||||
devhelp:
|
||||
$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
|
||||
@echo
|
||||
@echo "Build finished."
|
||||
@echo "To view the help file:"
|
||||
@echo "# mkdir -p $$HOME/.local/share/devhelp/TeamspeakStats"
|
||||
@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/TeamspeakStats"
|
||||
@echo "# devhelp"
|
||||
|
||||
epub:
|
||||
$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
|
||||
@echo
|
||||
@echo "Build finished. The epub file is in $(BUILDDIR)/epub."
|
||||
|
||||
latex:
|
||||
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
|
||||
@echo
|
||||
@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
|
||||
@echo "Run \`make' in that directory to run these through (pdf)latex" \
|
||||
"(use \`make latexpdf' here to do that automatically)."
|
||||
|
||||
latexpdf:
|
||||
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
|
||||
@echo "Running LaTeX files through pdflatex..."
|
||||
$(MAKE) -C $(BUILDDIR)/latex all-pdf
|
||||
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
|
||||
|
||||
latexpdfja:
|
||||
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
|
||||
@echo "Running LaTeX files through platex and dvipdfmx..."
|
||||
$(MAKE) -C $(BUILDDIR)/latex all-pdf-ja
|
||||
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
|
||||
|
||||
text:
|
||||
$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
|
||||
@echo
|
||||
@echo "Build finished. The text files are in $(BUILDDIR)/text."
|
||||
|
||||
man:
|
||||
$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
|
||||
@echo
|
||||
@echo "Build finished. The manual pages are in $(BUILDDIR)/man."
|
||||
|
||||
texinfo:
|
||||
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
|
||||
@echo
|
||||
@echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
|
||||
@echo "Run \`make' in that directory to run these through makeinfo" \
|
||||
"(use \`make info' here to do that automatically)."
|
||||
|
||||
info:
|
||||
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
|
||||
@echo "Running Texinfo files through makeinfo..."
|
||||
make -C $(BUILDDIR)/texinfo info
|
||||
@echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
|
||||
|
||||
gettext:
|
||||
$(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
|
||||
@echo
|
||||
@echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
|
||||
|
||||
changes:
|
||||
$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
|
||||
@echo
|
||||
@echo "The overview file is in $(BUILDDIR)/changes."
|
||||
|
||||
linkcheck:
|
||||
$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
|
||||
@echo
|
||||
@echo "Link check complete; look for any errors in the above output " \
|
||||
"or in $(BUILDDIR)/linkcheck/output.txt."
|
||||
|
||||
doctest:
|
||||
$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
|
||||
@echo "Testing of doctests in the sources finished, look at the " \
|
||||
"results in $(BUILDDIR)/doctest/output.txt."
|
||||
|
||||
coverage:
|
||||
$(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage
|
||||
@echo "Testing of coverage in the sources finished, look at the " \
|
||||
"results in $(BUILDDIR)/coverage/python.txt."
|
||||
|
||||
xml:
|
||||
$(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml
|
||||
@echo
|
||||
@echo "Build finished. The XML files are in $(BUILDDIR)/xml."
|
||||
|
||||
pseudoxml:
|
||||
$(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml
|
||||
@echo
|
||||
@echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml."
|
||||
# Catch-all target: route all unknown targets to Sphinx using the new
|
||||
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
|
||||
%: Makefile
|
||||
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
|
@ -1,14 +1,14 @@
|
|||
API
|
||||
***
|
||||
===
|
||||
|
||||
Log
|
||||
===
|
||||
---
|
||||
|
||||
.. automodule:: tsstats.log
|
||||
:members:
|
||||
|
||||
Client
|
||||
======
|
||||
------
|
||||
.. autoclass:: tsstats.client.Client
|
||||
:members:
|
||||
|
||||
|
@ -21,24 +21,24 @@ Client
|
|||
.. automethod:: tsstats.client.Clients.__iter__
|
||||
|
||||
Template
|
||||
========
|
||||
--------
|
||||
|
||||
.. automodule:: tsstats.template
|
||||
:members:
|
||||
|
||||
Config
|
||||
======
|
||||
------
|
||||
|
||||
.. automodule:: tsstats.config
|
||||
:members:
|
||||
|
||||
Exceptions
|
||||
==========
|
||||
----------
|
||||
.. automodule:: tsstats.exceptions
|
||||
:members:
|
||||
|
||||
Utils
|
||||
=====
|
||||
-----
|
||||
|
||||
.. automodule:: tsstats.utils
|
||||
:members:
|
30
docs/cli.rst
Normal file
30
docs/cli.rst
Normal file
|
@ -0,0 +1,30 @@
|
|||
Command Line Interface
|
||||
======================
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ tsstats --help
|
||||
usage: tsstats [-h] [-c CONFIG] [--idmap IDMAP] [-l LOG] [-o OUTPUT] [-d]
|
||||
[-ds] [-nod] [-t TEMPLATE] [-dtf DATETIMEFORMAT]
|
||||
[-otth ONLINETIMETHRESHOLD]
|
||||
|
||||
A simple Teamspeak stats-generator, based solely on server-logs
|
||||
|
||||
optional arguments:
|
||||
-h, --help show this help message and exit
|
||||
-c CONFIG, --config CONFIG
|
||||
path to config
|
||||
--idmap IDMAP path to id_map
|
||||
-l LOG, --log LOG path to your logfile(s). pass a directory to use all
|
||||
logfiles inside it
|
||||
-o OUTPUT, --output OUTPUT
|
||||
path to the output-file
|
||||
-d, --debug debug mode
|
||||
-ds, --debugstdout write debug output to stdout
|
||||
-nod, --noonlinedc don't add connect until now to onlinetime
|
||||
-t TEMPLATE, --template TEMPLATE
|
||||
path to custom template
|
||||
-dtf DATETIMEFORMAT, --datetimeformat DATETIMEFORMAT
|
||||
format of date/time-values (datetime.strftime)
|
||||
-otth ONLINETIMETHRESHOLD, --onlinetimethreshold ONLINETIMETHRESHOLD
|
||||
threshold for displaying onlinetime (in seconds)
|
|
@ -1,8 +1,7 @@
|
|||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# TeamspeakStats documentation build configuration file, created by
|
||||
# sphinx-quickstart on Thu May 12 21:42:11 2016.
|
||||
# sphinx-quickstart on Tue Apr 18 22:51:54 2017.
|
||||
#
|
||||
# This file is execfile()d with the current directory set to its
|
||||
# containing dir.
|
||||
|
@ -13,50 +12,60 @@
|
|||
# All configuration values have a default; values that are commented out
|
||||
# serve to show the default.
|
||||
|
||||
import sys
|
||||
import os
|
||||
|
||||
# If extensions (or modules to document with autodoc) are in another directory,
|
||||
# add these directories to sys.path here. If the directory is relative to the
|
||||
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
||||
sys.path.insert(0, os.path.abspath('../..'))
|
||||
#
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, os.path.abspath('..'))
|
||||
|
||||
|
||||
# -- General configuration ------------------------------------------------
|
||||
autodoc_member_order = 'bysource'
|
||||
|
||||
# If your documentation needs a minimal Sphinx version, state it here.
|
||||
#
|
||||
# needs_sphinx = '1.0'
|
||||
|
||||
# Add any Sphinx extension module names here, as strings. They can be
|
||||
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
|
||||
# ones.
|
||||
extensions = [
|
||||
'sphinx.ext.autodoc',
|
||||
'sphinx.ext.coverage',
|
||||
'sphinx.ext.viewcode',
|
||||
'sphinx.ext.viewcode'
|
||||
]
|
||||
|
||||
# Add any paths that contain templates here, relative to this directory.
|
||||
templates_path = ['_templates']
|
||||
|
||||
# The suffix(es) of source filenames.
|
||||
# You can specify multiple suffix as a list of string:
|
||||
#
|
||||
# source_suffix = ['.rst', '.md']
|
||||
source_suffix = '.rst'
|
||||
|
||||
# The encoding of source files.
|
||||
source_encoding = 'utf-8-sig'
|
||||
|
||||
# The master toctree document.
|
||||
master_doc = 'index'
|
||||
|
||||
# General information about the project.
|
||||
project = 'TeamspeakStats'
|
||||
copyright = '2016, Thor77'
|
||||
author = 'Thor77'
|
||||
project = u'TeamspeakStats'
|
||||
copyright = u'2017, Thor77'
|
||||
author = u'Thor77'
|
||||
|
||||
# The version info for the project you're documenting, acts as replacement for
|
||||
# |version| and |release|, also used in various other places throughout the
|
||||
# built documents.
|
||||
#
|
||||
# The short X.Y version.
|
||||
version = '0.6'
|
||||
version = u'1.4'
|
||||
# The full version, including alpha/beta/rc tags.
|
||||
release = '0.6.6'
|
||||
release = u'1.4.1'
|
||||
|
||||
# suppres warning about nonlocal images
|
||||
suppress_warnings = ['image.nonlocal_uri']
|
||||
|
||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||
# for a list of supported languages.
|
||||
|
@ -67,7 +76,8 @@ language = None
|
|||
|
||||
# List of patterns, relative to source directory, that match files and
|
||||
# directories to ignore when looking for source files.
|
||||
exclude_patterns = []
|
||||
# This patterns also effect to html_static_path and html_extra_path
|
||||
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
|
||||
|
||||
# The name of the Pygments (syntax highlighting) style to use.
|
||||
pygments_style = 'sphinx'
|
||||
|
@ -80,19 +90,57 @@ todo_include_todos = False
|
|||
|
||||
# The theme to use for HTML and HTML Help pages. See the documentation for
|
||||
# a list of builtin themes.
|
||||
html_theme = 'alabaster'
|
||||
#
|
||||
if os.environ.get('READTHEDOCS') != 'True':
|
||||
# we're not built by rtd => add rtd-theme
|
||||
import sphinx_rtd_theme
|
||||
html_theme = 'sphinx_rtd_theme'
|
||||
html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
|
||||
|
||||
# Theme options are theme-specific and customize the look and feel of a theme
|
||||
# further. For a list of options available for each theme, see the
|
||||
# documentation.
|
||||
#
|
||||
# html_theme_options = {}
|
||||
|
||||
# Add any paths that contain custom static files (such as style sheets) here,
|
||||
# relative to this directory. They are copied after the builtin static files,
|
||||
# so a file named "default.css" will overwrite the builtin "default.css".
|
||||
# html_static_path = ['_static']
|
||||
|
||||
|
||||
# -- Options for HTMLHelp output ------------------------------------------
|
||||
|
||||
# Output file base name for HTML help builder.
|
||||
htmlhelp_basename = 'TeamspeakStatsdoc'
|
||||
|
||||
|
||||
# -- Options for LaTeX output ---------------------------------------------
|
||||
|
||||
latex_elements = {
|
||||
# The paper size ('letterpaper' or 'a4paper').
|
||||
#
|
||||
# 'papersize': 'letterpaper',
|
||||
|
||||
# The font size ('10pt', '11pt' or '12pt').
|
||||
#
|
||||
# 'pointsize': '10pt',
|
||||
|
||||
# Additional stuff for the LaTeX preamble.
|
||||
#
|
||||
# 'preamble': '',
|
||||
|
||||
# Latex figure (float) alignment
|
||||
#
|
||||
# 'figure_align': 'htbp',
|
||||
}
|
||||
|
||||
# Grouping the document tree into LaTeX files. List of tuples
|
||||
# (source start file, target name, title,
|
||||
# author, documentclass [howto, manual, or own class]).
|
||||
latex_documents = [
|
||||
(master_doc, 'TeamspeakStats.tex', 'TeamspeakStats Documentation',
|
||||
'Thor77', 'manual'),
|
||||
(master_doc, 'TeamspeakStats.tex', u'TeamspeakStats Documentation',
|
||||
u'Thor77', 'manual'),
|
||||
]
|
||||
|
||||
|
||||
|
@ -101,7 +149,7 @@ latex_documents = [
|
|||
# One entry per manual page. List of tuples
|
||||
# (source start file, name, description, authors, manual section).
|
||||
man_pages = [
|
||||
(master_doc, 'teamspeakstats', 'TeamspeakStats Documentation',
|
||||
(master_doc, 'teamspeakstats', u'TeamspeakStats Documentation',
|
||||
[author], 1)
|
||||
]
|
||||
|
||||
|
@ -112,7 +160,7 @@ man_pages = [
|
|||
# (source start file, target name, title, author,
|
||||
# dir menu entry, description, category)
|
||||
texinfo_documents = [
|
||||
(master_doc, 'TeamspeakStats', 'TeamspeakStats Documentation',
|
||||
author, 'TeamspeakStats', 'One line description of project.',
|
||||
'Miscellaneous'),
|
||||
(master_doc, 'TeamspeakStats', u'TeamspeakStats Documentation',
|
||||
author, 'TeamspeakStats', 'One line description of project.',
|
||||
'Miscellaneous'),
|
||||
]
|
40
docs/config.rst
Normal file
40
docs/config.rst
Normal file
|
@ -0,0 +1,40 @@
|
|||
Config
|
||||
======
|
||||
|
||||
The configfile is using the .ini-format.
|
||||
Currently all settings are read from the ``[General]``-section.
|
||||
|
||||
+---------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
|
||||
| Key | Description |
|
||||
+=====================+=================================================================================================================================================================================+
|
||||
| log | Path to TS3Server-logfile(s) (supports `globbing <https://docs.python.org/3/library/glob.html>`__) |
|
||||
+---------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
|
||||
| output | Path to the location, where the generator will put the generated .html-file |
|
||||
+---------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
|
||||
| idmap | Path to `IdentMap <https://teamspeakstats.readthedocs.io/en/latest/identmap.html>`__ |
|
||||
+---------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
|
||||
| debug | debug mode |
|
||||
+---------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
|
||||
| onlinedc | Add timedelta from last-connect until now to onlinetime for connected clients |
|
||||
+---------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
|
||||
| template | Path to a custom template file (relative from ``tsstats/`` or absolute) |
|
||||
+---------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
|
||||
| datetimeformat | Format of date/time-values used for render-timestamp and last online (using `datetime.strftime <https://docs.python.org/3/library/datetime.html#strftime-strptime-behavior>`__) |
|
||||
+---------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
|
||||
| onlinetimethreshold | Clients with an onlinetime below that threshold (in seconds) are hidden in the onlinetime-section |
|
||||
+---------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
|
||||
|
||||
Example
|
||||
-------
|
||||
|
||||
``config.ini``
|
||||
|
||||
.. code-block:: ini
|
||||
|
||||
[General]
|
||||
log = /usr/local/bin/teamspeak-server/logs/ts3server*_1.log
|
||||
output = /var/www/html/stats.html
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ tsstats -c config.ini
|
35
docs/development.rst
Normal file
35
docs/development.rst
Normal file
|
@ -0,0 +1,35 @@
|
|||
Development
|
||||
===========
|
||||
|
||||
Contributing
|
||||
------------
|
||||
Contributions are very welcome!
|
||||
|
||||
Before developing a new (possibly breaking) feature, please open an Issue about it first
|
||||
so we can discuss your idea and possible implementations.
|
||||
|
||||
Please read this document carefully before submitting your Pull Request to avoid failing CI tests.
|
||||
|
||||
Style
|
||||
-----
|
||||
Your contribution should pass `flake8 <https://flake8.readthedocs.io>`__
|
||||
as well as `isort <https://github.com/timothycrosley/isort>`__.
|
||||
|
||||
Testing
|
||||
-------
|
||||
There are unit tests for all parts of the project built with `py.test <https://docs.pytest.org>`__.
|
||||
Besides ``py.test`` tests require ``BeautifulSoup`` for template-testing.
|
||||
Those requirements are listed in ``testing_requirements.txt``::
|
||||
|
||||
$ pip install -r requirements-dev.txt
|
||||
$ py.test tsstats/tests/
|
||||
|
||||
Versioning
|
||||
----------
|
||||
TeamspeakStats uses `Semantic Versioning <http://semver.org/>`__.
|
||||
Please don't bump versions in your Pull Requests, though, we will do that after merging.
|
||||
|
||||
Python Versions
|
||||
---------------
|
||||
To keep the tool accessible and maintainable at the same time at least ``Python 2.7`` is required,
|
||||
so keep this in mind when using fancy new features from a recent Python version.
|
|
@ -1,20 +1,42 @@
|
|||
IdentMap
|
||||
********
|
||||
========
|
||||
|
||||
An IdentMap is used to map multiple (U)ID's of one client to one client.
|
||||
This can be useful, if a user creates multiple identities and you want to summarize all actions from all identities.
|
||||
To pass an IdentMap to TeamspeakStats, create your IdentMap as shown above and pass it to the cli::
|
||||
|
||||
tsstats --idmap <path to idmap.json>
|
||||
|
||||
TeamspeakStats' IdentMap-file is saved in json-format::
|
||||
|
||||
[
|
||||
{
|
||||
"primary_id": "1",
|
||||
"alternate_ids": ["2", "3", "4"]
|
||||
}
|
||||
]
|
||||
|
||||
If you would pass this IdentMap to TeamspeakStats and your log would contain entries for clients with id's 1, 2, 3 and 4,
|
||||
your output will just show data for one client (1).
|
||||
|
||||
The format is flexible enough to support other arbitrary data to assist you in maintaining your IdentMap::
|
||||
|
||||
[
|
||||
{
|
||||
"name": "Friend 1",
|
||||
"primary_id": "1",
|
||||
"alternate_ids": ["2", "3", "4"]
|
||||
}
|
||||
]
|
||||
|
||||
The parser will ignore all nodes other than the "primary_id" and "alternate_ids" nodes, allowing you to use them for whatever you want.
|
||||
|
||||
The original IdentMap format is still supported::
|
||||
|
||||
{
|
||||
'2': '1',
|
||||
'3': '1',
|
||||
'4': '1'
|
||||
}
|
||||
|
||||
|
||||
If you would pass this IdentMap to TeamspeakStats and your log would contain entries for clients with id's 1, 2, 3 and 4,
|
||||
your output will just show data for one client (1).
|
||||
|
||||
To pass an IdentMap to TeamspeakStats, create your IdentMap as shown above and pass it to the cli::
|
||||
|
||||
tsstats --idmap <path to idmap.json>
|
||||
While it is less expressive, it is also less verbose.
|
13
docs/index.rst
Normal file
13
docs/index.rst
Normal file
|
@ -0,0 +1,13 @@
|
|||
TeamspeakStats Documentation
|
||||
============================
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
cli
|
||||
config
|
||||
identmap
|
||||
api
|
||||
development
|
||||
|
||||
.. include:: ../README.rst
|
|
@ -1,2 +0,0 @@
|
|||
Command-Line-Interface
|
||||
**********************
|
|
@ -1,2 +0,0 @@
|
|||
Config
|
||||
******
|
|
@ -1,16 +0,0 @@
|
|||
Welcome to TeamspeakStats's documentation!
|
||||
==========================================
|
||||
|
||||
This document contains a more in-depth and technical documentation (in contrast to the `README <https://github.com/Thor77/TeamspeakStats/blob/master/README.md>`_)
|
||||
for TeamspeakStats.
|
||||
|
||||
Contents:
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
quickstart
|
||||
cli
|
||||
config
|
||||
identmap
|
||||
api
|
|
@ -1,30 +0,0 @@
|
|||
Quickstart
|
||||
**********
|
||||
First, you have to get and install a decent version of `Python <https://python.org/>`_ (at least 2.7).
|
||||
Now you have various options to use TeamspeakStats, going from easy to hard:
|
||||
|
||||
1. Install the tool via pip ``pip install tsstats``
|
||||
2. Clone the sourcecode ``git clone https://github.com/Thor77/TeamspeakStats``
|
||||
|
||||
* **A** Just run the script from your local copy ``python -m tsstats``
|
||||
* **B** Install the script with the included *setup.py* ``python setup.py install``
|
||||
|
||||
To start, you can just use cli-arguments to control the behaviour of TeamspeakStats::
|
||||
|
||||
usage: tsstats [-h] [-c CONFIG] [--idmap IDMAP] [-l LOG] [-o OUTPUT] [-d]
|
||||
|
||||
A simple Teamspeak stats-generator - based on server-logs
|
||||
|
||||
optional arguments:
|
||||
-h, --help show this help message and exit
|
||||
-c CONFIG, --config CONFIG
|
||||
path to config
|
||||
--idmap IDMAP path to id_map
|
||||
-l LOG, --log LOG path to your logfile(s)
|
||||
-o OUTPUT, --output OUTPUT
|
||||
path to the output-file
|
||||
-d, --debug debug mode
|
||||
|
||||
Take a look at :doc:`cli` to get a more in-depth explanation of the available flags.
|
||||
If you want to use TeamspeakStats in a script (or cron) you should consider creating
|
||||
a :doc:`config`.
|
512
poetry.lock
generated
Normal file
512
poetry.lock
generated
Normal file
|
@ -0,0 +1,512 @@
|
|||
# This file is automatically @generated by Poetry and should not be changed by hand.
|
||||
|
||||
[[package]]
|
||||
name = "attrs"
|
||||
version = "22.2.0"
|
||||
description = "Classes Without Boilerplate"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
files = [
|
||||
{file = "attrs-22.2.0-py3-none-any.whl", hash = "sha256:29e95c7f6778868dbd49170f98f8818f78f3dc5e0e37c0b1f474e3561b240836"},
|
||||
{file = "attrs-22.2.0.tar.gz", hash = "sha256:c9227bfc2f01993c03f68db37d1d15c9690188323c067c641f1a35ca58185f99"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
cov = ["attrs[tests]", "coverage-enable-subprocess", "coverage[toml] (>=5.3)"]
|
||||
dev = ["attrs[docs,tests]"]
|
||||
docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope.interface"]
|
||||
tests = ["attrs[tests-no-zope]", "zope.interface"]
|
||||
tests-no-zope = ["cloudpickle", "cloudpickle", "hypothesis", "hypothesis", "mypy (>=0.971,<0.990)", "mypy (>=0.971,<0.990)", "pympler", "pympler", "pytest (>=4.3.0)", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-mypy-plugins", "pytest-xdist[psutil]", "pytest-xdist[psutil]"]
|
||||
|
||||
[[package]]
|
||||
name = "beautifulsoup4"
|
||||
version = "4.11.1"
|
||||
description = "Screen-scraping library"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.6.0"
|
||||
files = [
|
||||
{file = "beautifulsoup4-4.11.1-py3-none-any.whl", hash = "sha256:58d5c3d29f5a36ffeb94f02f0d786cd53014cf9b3b3951d42e0080d8a9498d30"},
|
||||
{file = "beautifulsoup4-4.11.1.tar.gz", hash = "sha256:ad9aa55b65ef2808eb405f46cf74df7fcb7044d5cbc26487f96eb2ef2e436693"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
soupsieve = ">1.2"
|
||||
|
||||
[package.extras]
|
||||
html5lib = ["html5lib"]
|
||||
lxml = ["lxml"]
|
||||
|
||||
[[package]]
|
||||
name = "colorama"
|
||||
version = "0.4.6"
|
||||
description = "Cross-platform colored terminal text."
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
|
||||
files = [
|
||||
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
|
||||
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "coverage"
|
||||
version = "7.0.5"
|
||||
description = "Code coverage measurement for Python"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "coverage-7.0.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2a7f23bbaeb2a87f90f607730b45564076d870f1fb07b9318d0c21f36871932b"},
|
||||
{file = "coverage-7.0.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c18d47f314b950dbf24a41787ced1474e01ca816011925976d90a88b27c22b89"},
|
||||
{file = "coverage-7.0.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ef14d75d86f104f03dea66c13188487151760ef25dd6b2dbd541885185f05f40"},
|
||||
{file = "coverage-7.0.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66e50680e888840c0995f2ad766e726ce71ca682e3c5f4eee82272c7671d38a2"},
|
||||
{file = "coverage-7.0.5-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a9fed35ca8c6e946e877893bbac022e8563b94404a605af1d1e6accc7eb73289"},
|
||||
{file = "coverage-7.0.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d8d04e755934195bdc1db45ba9e040b8d20d046d04d6d77e71b3b34a8cc002d0"},
|
||||
{file = "coverage-7.0.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:7e109f1c9a3ece676597831874126555997c48f62bddbcace6ed17be3e372de8"},
|
||||
{file = "coverage-7.0.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0a1890fca2962c4f1ad16551d660b46ea77291fba2cc21c024cd527b9d9c8809"},
|
||||
{file = "coverage-7.0.5-cp310-cp310-win32.whl", hash = "sha256:be9fcf32c010da0ba40bf4ee01889d6c737658f4ddff160bd7eb9cac8f094b21"},
|
||||
{file = "coverage-7.0.5-cp310-cp310-win_amd64.whl", hash = "sha256:cbfcba14a3225b055a28b3199c3d81cd0ab37d2353ffd7f6fd64844cebab31ad"},
|
||||
{file = "coverage-7.0.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:30b5fec1d34cc932c1bc04017b538ce16bf84e239378b8f75220478645d11fca"},
|
||||
{file = "coverage-7.0.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1caed2367b32cc80a2b7f58a9f46658218a19c6cfe5bc234021966dc3daa01f0"},
|
||||
{file = "coverage-7.0.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d254666d29540a72d17cc0175746cfb03d5123db33e67d1020e42dae611dc196"},
|
||||
{file = "coverage-7.0.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:19245c249aa711d954623d94f23cc94c0fd65865661f20b7781210cb97c471c0"},
|
||||
{file = "coverage-7.0.5-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b05ed4b35bf6ee790832f68932baf1f00caa32283d66cc4d455c9e9d115aafc"},
|
||||
{file = "coverage-7.0.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:29de916ba1099ba2aab76aca101580006adfac5646de9b7c010a0f13867cba45"},
|
||||
{file = "coverage-7.0.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:e057e74e53db78122a3979f908973e171909a58ac20df05c33998d52e6d35757"},
|
||||
{file = "coverage-7.0.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:411d4ff9d041be08fdfc02adf62e89c735b9468f6d8f6427f8a14b6bb0a85095"},
|
||||
{file = "coverage-7.0.5-cp311-cp311-win32.whl", hash = "sha256:52ab14b9e09ce052237dfe12d6892dd39b0401690856bcfe75d5baba4bfe2831"},
|
||||
{file = "coverage-7.0.5-cp311-cp311-win_amd64.whl", hash = "sha256:1f66862d3a41674ebd8d1a7b6f5387fe5ce353f8719040a986551a545d7d83ea"},
|
||||
{file = "coverage-7.0.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b69522b168a6b64edf0c33ba53eac491c0a8f5cc94fa4337f9c6f4c8f2f5296c"},
|
||||
{file = "coverage-7.0.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:436e103950d05b7d7f55e39beeb4d5be298ca3e119e0589c0227e6d0b01ee8c7"},
|
||||
{file = "coverage-7.0.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b8c56bec53d6e3154eaff6ea941226e7bd7cc0d99f9b3756c2520fc7a94e6d96"},
|
||||
{file = "coverage-7.0.5-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a38362528a9115a4e276e65eeabf67dcfaf57698e17ae388599568a78dcb029"},
|
||||
{file = "coverage-7.0.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:f67472c09a0c7486e27f3275f617c964d25e35727af952869dd496b9b5b7f6a3"},
|
||||
{file = "coverage-7.0.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:220e3fa77d14c8a507b2d951e463b57a1f7810a6443a26f9b7591ef39047b1b2"},
|
||||
{file = "coverage-7.0.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ecb0f73954892f98611e183f50acdc9e21a4653f294dfbe079da73c6378a6f47"},
|
||||
{file = "coverage-7.0.5-cp37-cp37m-win32.whl", hash = "sha256:d8f3e2e0a1d6777e58e834fd5a04657f66affa615dae61dd67c35d1568c38882"},
|
||||
{file = "coverage-7.0.5-cp37-cp37m-win_amd64.whl", hash = "sha256:9e662e6fc4f513b79da5d10a23edd2b87685815b337b1a30cd11307a6679148d"},
|
||||
{file = "coverage-7.0.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:790e4433962c9f454e213b21b0fd4b42310ade9c077e8edcb5113db0818450cb"},
|
||||
{file = "coverage-7.0.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:49640bda9bda35b057b0e65b7c43ba706fa2335c9a9896652aebe0fa399e80e6"},
|
||||
{file = "coverage-7.0.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d66187792bfe56f8c18ba986a0e4ae44856b1c645336bd2c776e3386da91e1dd"},
|
||||
{file = "coverage-7.0.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:276f4cd0001cd83b00817c8db76730938b1ee40f4993b6a905f40a7278103b3a"},
|
||||
{file = "coverage-7.0.5-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95304068686545aa368b35dfda1cdfbbdbe2f6fe43de4a2e9baa8ebd71be46e2"},
|
||||
{file = "coverage-7.0.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:17e01dd8666c445025c29684d4aabf5a90dc6ef1ab25328aa52bedaa95b65ad7"},
|
||||
{file = "coverage-7.0.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:ea76dbcad0b7b0deb265d8c36e0801abcddf6cc1395940a24e3595288b405ca0"},
|
||||
{file = "coverage-7.0.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:50a6adc2be8edd7ee67d1abc3cd20678987c7b9d79cd265de55941e3d0d56499"},
|
||||
{file = "coverage-7.0.5-cp38-cp38-win32.whl", hash = "sha256:e4ce984133b888cc3a46867c8b4372c7dee9cee300335e2925e197bcd45b9e16"},
|
||||
{file = "coverage-7.0.5-cp38-cp38-win_amd64.whl", hash = "sha256:4a950f83fd3f9bca23b77442f3a2b2ea4ac900944d8af9993743774c4fdc57af"},
|
||||
{file = "coverage-7.0.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3c2155943896ac78b9b0fd910fb381186d0c345911f5333ee46ac44c8f0e43ab"},
|
||||
{file = "coverage-7.0.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:54f7e9705e14b2c9f6abdeb127c390f679f6dbe64ba732788d3015f7f76ef637"},
|
||||
{file = "coverage-7.0.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ee30375b409d9a7ea0f30c50645d436b6f5dfee254edffd27e45a980ad2c7f4"},
|
||||
{file = "coverage-7.0.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b78729038abea6a5df0d2708dce21e82073463b2d79d10884d7d591e0f385ded"},
|
||||
{file = "coverage-7.0.5-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13250b1f0bd023e0c9f11838bdeb60214dd5b6aaf8e8d2f110c7e232a1bff83b"},
|
||||
{file = "coverage-7.0.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:2c407b1950b2d2ffa091f4e225ca19a66a9bd81222f27c56bd12658fc5ca1209"},
|
||||
{file = "coverage-7.0.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:c76a3075e96b9c9ff00df8b5f7f560f5634dffd1658bafb79eb2682867e94f78"},
|
||||
{file = "coverage-7.0.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:f26648e1b3b03b6022b48a9b910d0ae209e2d51f50441db5dce5b530fad6d9b1"},
|
||||
{file = "coverage-7.0.5-cp39-cp39-win32.whl", hash = "sha256:ba3027deb7abf02859aca49c865ece538aee56dcb4871b4cced23ba4d5088904"},
|
||||
{file = "coverage-7.0.5-cp39-cp39-win_amd64.whl", hash = "sha256:949844af60ee96a376aac1ded2a27e134b8c8d35cc006a52903fc06c24a3296f"},
|
||||
{file = "coverage-7.0.5-pp37.pp38.pp39-none-any.whl", hash = "sha256:b9727ac4f5cf2cbf87880a63870b5b9730a8ae3a4a360241a0fdaa2f71240ff0"},
|
||||
{file = "coverage-7.0.5.tar.gz", hash = "sha256:051afcbd6d2ac39298d62d340f94dbb6a1f31de06dfaf6fcef7b759dd3860c45"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""}
|
||||
|
||||
[package.extras]
|
||||
toml = ["tomli"]
|
||||
|
||||
[[package]]
|
||||
name = "exceptiongroup"
|
||||
version = "1.1.0"
|
||||
description = "Backport of PEP 654 (exception groups)"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "exceptiongroup-1.1.0-py3-none-any.whl", hash = "sha256:327cbda3da756e2de031a3107b81ab7b3770a602c4d16ca618298c526f4bec1e"},
|
||||
{file = "exceptiongroup-1.1.0.tar.gz", hash = "sha256:bcb67d800a4497e1b404c2dd44fca47d3b7a5e5433dbab67f96c1a685cdfdf23"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
test = ["pytest (>=6)"]
|
||||
|
||||
[[package]]
|
||||
name = "iniconfig"
|
||||
version = "2.0.0"
|
||||
description = "brain-dead simple config-ini parsing"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"},
|
||||
{file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jinja2"
|
||||
version = "3.1.2"
|
||||
description = "A very fast and expressive template engine."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"},
|
||||
{file = "Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
MarkupSafe = ">=2.0"
|
||||
|
||||
[package.extras]
|
||||
i18n = ["Babel (>=2.7)"]
|
||||
|
||||
[[package]]
|
||||
name = "markupsafe"
|
||||
version = "2.1.1"
|
||||
description = "Safely add untrusted strings to HTML/XML markup."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:86b1f75c4e7c2ac2ccdaec2b9022845dbb81880ca318bb7a0a01fbf7813e3812"},
|
||||
{file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f121a1420d4e173a5d96e47e9a0c0dcff965afdf1626d28de1460815f7c4ee7a"},
|
||||
{file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a49907dd8420c5685cfa064a1335b6754b74541bbb3706c259c02ed65b644b3e"},
|
||||
{file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10c1bfff05d95783da83491be968e8fe789263689c02724e0c691933c52994f5"},
|
||||
{file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b7bd98b796e2b6553da7225aeb61f447f80a1ca64f41d83612e6139ca5213aa4"},
|
||||
{file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b09bf97215625a311f669476f44b8b318b075847b49316d3e28c08e41a7a573f"},
|
||||
{file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:694deca8d702d5db21ec83983ce0bb4b26a578e71fbdbd4fdcd387daa90e4d5e"},
|
||||
{file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:efc1913fd2ca4f334418481c7e595c00aad186563bbc1ec76067848c7ca0a933"},
|
||||
{file = "MarkupSafe-2.1.1-cp310-cp310-win32.whl", hash = "sha256:4a33dea2b688b3190ee12bd7cfa29d39c9ed176bda40bfa11099a3ce5d3a7ac6"},
|
||||
{file = "MarkupSafe-2.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:dda30ba7e87fbbb7eab1ec9f58678558fd9a6b8b853530e176eabd064da81417"},
|
||||
{file = "MarkupSafe-2.1.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:671cd1187ed5e62818414afe79ed29da836dde67166a9fac6d435873c44fdd02"},
|
||||
{file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3799351e2336dc91ea70b034983ee71cf2f9533cdff7c14c90ea126bfd95d65a"},
|
||||
{file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e72591e9ecd94d7feb70c1cbd7be7b3ebea3f548870aa91e2732960fa4d57a37"},
|
||||
{file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6fbf47b5d3728c6aea2abb0589b5d30459e369baa772e0f37a0320185e87c980"},
|
||||
{file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d5ee4f386140395a2c818d149221149c54849dfcfcb9f1debfe07a8b8bd63f9a"},
|
||||
{file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:bcb3ed405ed3222f9904899563d6fc492ff75cce56cba05e32eff40e6acbeaa3"},
|
||||
{file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:e1c0b87e09fa55a220f058d1d49d3fb8df88fbfab58558f1198e08c1e1de842a"},
|
||||
{file = "MarkupSafe-2.1.1-cp37-cp37m-win32.whl", hash = "sha256:8dc1c72a69aa7e082593c4a203dcf94ddb74bb5c8a731e4e1eb68d031e8498ff"},
|
||||
{file = "MarkupSafe-2.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:97a68e6ada378df82bc9f16b800ab77cbf4b2fada0081794318520138c088e4a"},
|
||||
{file = "MarkupSafe-2.1.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e8c843bbcda3a2f1e3c2ab25913c80a3c5376cd00c6e8c4a86a89a28c8dc5452"},
|
||||
{file = "MarkupSafe-2.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0212a68688482dc52b2d45013df70d169f542b7394fc744c02a57374a4207003"},
|
||||
{file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e576a51ad59e4bfaac456023a78f6b5e6e7651dcd383bcc3e18d06f9b55d6d1"},
|
||||
{file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b9fe39a2ccc108a4accc2676e77da025ce383c108593d65cc909add5c3bd601"},
|
||||
{file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:96e37a3dc86e80bf81758c152fe66dbf60ed5eca3d26305edf01892257049925"},
|
||||
{file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6d0072fea50feec76a4c418096652f2c3238eaa014b2f94aeb1d56a66b41403f"},
|
||||
{file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:089cf3dbf0cd6c100f02945abeb18484bd1ee57a079aefd52cffd17fba910b88"},
|
||||
{file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6a074d34ee7a5ce3effbc526b7083ec9731bb3cbf921bbe1d3005d4d2bdb3a63"},
|
||||
{file = "MarkupSafe-2.1.1-cp38-cp38-win32.whl", hash = "sha256:421be9fbf0ffe9ffd7a378aafebbf6f4602d564d34be190fc19a193232fd12b1"},
|
||||
{file = "MarkupSafe-2.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:fc7b548b17d238737688817ab67deebb30e8073c95749d55538ed473130ec0c7"},
|
||||
{file = "MarkupSafe-2.1.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e04e26803c9c3851c931eac40c695602c6295b8d432cbe78609649ad9bd2da8a"},
|
||||
{file = "MarkupSafe-2.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b87db4360013327109564f0e591bd2a3b318547bcef31b468a92ee504d07ae4f"},
|
||||
{file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99a2a507ed3ac881b975a2976d59f38c19386d128e7a9a18b7df6fff1fd4c1d6"},
|
||||
{file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56442863ed2b06d19c37f94d999035e15ee982988920e12a5b4ba29b62ad1f77"},
|
||||
{file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3ce11ee3f23f79dbd06fb3d63e2f6af7b12db1d46932fe7bd8afa259a5996603"},
|
||||
{file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:33b74d289bd2f5e527beadcaa3f401e0df0a89927c1559c8566c066fa4248ab7"},
|
||||
{file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:43093fb83d8343aac0b1baa75516da6092f58f41200907ef92448ecab8825135"},
|
||||
{file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8e3dcf21f367459434c18e71b2a9532d96547aef8a871872a5bd69a715c15f96"},
|
||||
{file = "MarkupSafe-2.1.1-cp39-cp39-win32.whl", hash = "sha256:d4306c36ca495956b6d568d276ac11fdd9c30a36f1b6eb928070dc5360b22e1c"},
|
||||
{file = "MarkupSafe-2.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:46d00d6cfecdde84d40e572d63735ef81423ad31184100411e6e3388d405e247"},
|
||||
{file = "MarkupSafe-2.1.1.tar.gz", hash = "sha256:7f91197cc9e48f989d12e4e6fbc46495c446636dfc81b9ccf50bb0ec74b91d4b"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mccabe"
|
||||
version = "0.7.0"
|
||||
description = "McCabe checker, plugin for flake8"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
files = [
|
||||
{file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"},
|
||||
{file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "packaging"
|
||||
version = "23.0"
|
||||
description = "Core utilities for Python packages"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "packaging-23.0-py3-none-any.whl", hash = "sha256:714ac14496c3e68c99c29b00845f7a2b85f3bb6f1078fd9f72fd20f0570002b2"},
|
||||
{file = "packaging-23.0.tar.gz", hash = "sha256:b6ad297f8907de0fa2fe1ccbd26fdaf387f5f47c7275fedf8cce89f99446cf97"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pastel"
|
||||
version = "0.2.1"
|
||||
description = "Bring colors to your terminal."
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
||||
files = [
|
||||
{file = "pastel-0.2.1-py2.py3-none-any.whl", hash = "sha256:4349225fcdf6c2bb34d483e523475de5bb04a5c10ef711263452cb37d7dd4364"},
|
||||
{file = "pastel-0.2.1.tar.gz", hash = "sha256:e6581ac04e973cac858828c6202c1e1e81fee1dc7de7683f3e1ffe0bfd8a573d"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pendulum"
|
||||
version = "2.1.2"
|
||||
description = "Python datetimes made easy"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
||||
files = [
|
||||
{file = "pendulum-2.1.2-cp27-cp27m-macosx_10_15_x86_64.whl", hash = "sha256:b6c352f4bd32dff1ea7066bd31ad0f71f8d8100b9ff709fb343f3b86cee43efe"},
|
||||
{file = "pendulum-2.1.2-cp27-cp27m-win_amd64.whl", hash = "sha256:318f72f62e8e23cd6660dbafe1e346950281a9aed144b5c596b2ddabc1d19739"},
|
||||
{file = "pendulum-2.1.2-cp35-cp35m-macosx_10_15_x86_64.whl", hash = "sha256:0731f0c661a3cb779d398803655494893c9f581f6488048b3fb629c2342b5394"},
|
||||
{file = "pendulum-2.1.2-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:3481fad1dc3f6f6738bd575a951d3c15d4b4ce7c82dce37cf8ac1483fde6e8b0"},
|
||||
{file = "pendulum-2.1.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:9702069c694306297ed362ce7e3c1ef8404ac8ede39f9b28b7c1a7ad8c3959e3"},
|
||||
{file = "pendulum-2.1.2-cp35-cp35m-win_amd64.whl", hash = "sha256:fb53ffa0085002ddd43b6ca61a7b34f2d4d7c3ed66f931fe599e1a531b42af9b"},
|
||||
{file = "pendulum-2.1.2-cp36-cp36m-macosx_10_15_x86_64.whl", hash = "sha256:c501749fdd3d6f9e726086bf0cd4437281ed47e7bca132ddb522f86a1645d360"},
|
||||
{file = "pendulum-2.1.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:c807a578a532eeb226150d5006f156632df2cc8c5693d778324b43ff8c515dd0"},
|
||||
{file = "pendulum-2.1.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:2d1619a721df661e506eff8db8614016f0720ac171fe80dda1333ee44e684087"},
|
||||
{file = "pendulum-2.1.2-cp36-cp36m-win_amd64.whl", hash = "sha256:f888f2d2909a414680a29ae74d0592758f2b9fcdee3549887779cd4055e975db"},
|
||||
{file = "pendulum-2.1.2-cp37-cp37m-macosx_10_15_x86_64.whl", hash = "sha256:e95d329384717c7bf627bf27e204bc3b15c8238fa8d9d9781d93712776c14002"},
|
||||
{file = "pendulum-2.1.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:4c9c689747f39d0d02a9f94fcee737b34a5773803a64a5fdb046ee9cac7442c5"},
|
||||
{file = "pendulum-2.1.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:1245cd0075a3c6d889f581f6325dd8404aca5884dea7223a5566c38aab94642b"},
|
||||
{file = "pendulum-2.1.2-cp37-cp37m-win_amd64.whl", hash = "sha256:db0a40d8bcd27b4fb46676e8eb3c732c67a5a5e6bfab8927028224fbced0b40b"},
|
||||
{file = "pendulum-2.1.2-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:f5e236e7730cab1644e1b87aca3d2ff3e375a608542e90fe25685dae46310116"},
|
||||
{file = "pendulum-2.1.2-cp38-cp38-manylinux1_i686.whl", hash = "sha256:de42ea3e2943171a9e95141f2eecf972480636e8e484ccffaf1e833929e9e052"},
|
||||
{file = "pendulum-2.1.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7c5ec650cb4bec4c63a89a0242cc8c3cebcec92fcfe937c417ba18277d8560be"},
|
||||
{file = "pendulum-2.1.2-cp38-cp38-win_amd64.whl", hash = "sha256:33fb61601083f3eb1d15edeb45274f73c63b3c44a8524703dc143f4212bf3269"},
|
||||
{file = "pendulum-2.1.2-cp39-cp39-manylinux1_i686.whl", hash = "sha256:29c40a6f2942376185728c9a0347d7c0f07905638c83007e1d262781f1e6953a"},
|
||||
{file = "pendulum-2.1.2-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:94b1fc947bfe38579b28e1cccb36f7e28a15e841f30384b5ad6c5e31055c85d7"},
|
||||
{file = "pendulum-2.1.2.tar.gz", hash = "sha256:b06a0ca1bfe41c990bbf0c029f0b6501a7f2ec4e38bfec730712015e8860f207"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
python-dateutil = ">=2.6,<3.0"
|
||||
pytzdata = ">=2020.1"
|
||||
|
||||
[[package]]
|
||||
name = "pluggy"
|
||||
version = "1.0.0"
|
||||
description = "plugin and hook calling mechanisms for python"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
files = [
|
||||
{file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"},
|
||||
{file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
dev = ["pre-commit", "tox"]
|
||||
testing = ["pytest", "pytest-benchmark"]
|
||||
|
||||
[[package]]
|
||||
name = "poethepoet"
|
||||
version = "0.18.0"
|
||||
description = "A task runner that works well with poetry."
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "poethepoet-0.18.0-py3-none-any.whl", hash = "sha256:62ba57982cc8303356e1535e047abb38e29b6fb8c8455c5920c4e678c24241bc"},
|
||||
{file = "poethepoet-0.18.0.tar.gz", hash = "sha256:ee2c8a71ac07dea7e415ab9790308f3425ff4423385f159b0b0b4b8f7eba8b84"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
pastel = ">=0.2.1,<0.3.0"
|
||||
tomli = ">=1.2.2"
|
||||
|
||||
[package.extras]
|
||||
poetry-plugin = ["poetry (>=1.0,<2.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "pycodestyle"
|
||||
version = "2.10.0"
|
||||
description = "Python style guide checker"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
files = [
|
||||
{file = "pycodestyle-2.10.0-py2.py3-none-any.whl", hash = "sha256:8a4eaf0d0495c7395bdab3589ac2db602797d76207242c17d470186815706610"},
|
||||
{file = "pycodestyle-2.10.0.tar.gz", hash = "sha256:347187bdb476329d98f695c213d7295a846d1152ff4fe9bacb8a9590b8ee7053"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pydocstyle"
|
||||
version = "6.2.3"
|
||||
description = "Python docstring style checker"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
files = [
|
||||
{file = "pydocstyle-6.2.3-py3-none-any.whl", hash = "sha256:a04ed1e6fe0be0970eddbb1681a7ab59b11eb92729fdb4b9b24f0eb11a25629e"},
|
||||
{file = "pydocstyle-6.2.3.tar.gz", hash = "sha256:d867acad25e48471f2ad8a40ef9813125e954ad675202245ca836cb6e28b2297"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
snowballstemmer = ">=2.2.0"
|
||||
|
||||
[package.extras]
|
||||
toml = ["tomli (>=1.2.3)"]
|
||||
|
||||
[[package]]
|
||||
name = "pyflakes"
|
||||
version = "3.0.1"
|
||||
description = "passive checker of Python programs"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
files = [
|
||||
{file = "pyflakes-3.0.1-py2.py3-none-any.whl", hash = "sha256:ec55bf7fe21fff7f1ad2f7da62363d749e2a470500eab1b555334b67aa1ef8cf"},
|
||||
{file = "pyflakes-3.0.1.tar.gz", hash = "sha256:ec8b276a6b60bd80defed25add7e439881c19e64850afd9b346283d4165fd0fd"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pylama"
|
||||
version = "8.4.1"
|
||||
description = "Code audit tool for python"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "pylama-8.4.1-py3-none-any.whl", hash = "sha256:5bbdbf5b620aba7206d688ed9fc917ecd3d73e15ec1a89647037a09fa3a86e60"},
|
||||
{file = "pylama-8.4.1.tar.gz", hash = "sha256:2d4f7aecfb5b7466216d48610c7d6bad1c3990c29cdd392ad08259b161e486f6"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
mccabe = ">=0.7.0"
|
||||
pycodestyle = ">=2.9.1"
|
||||
pydocstyle = ">=6.1.1"
|
||||
pyflakes = ">=2.5.0"
|
||||
|
||||
[package.extras]
|
||||
all = ["eradicate", "mypy", "pylint", "radon", "vulture"]
|
||||
eradicate = ["eradicate"]
|
||||
mypy = ["mypy"]
|
||||
pylint = ["pylint"]
|
||||
radon = ["radon"]
|
||||
tests = ["eradicate (>=2.0.0)", "mypy", "pylama-quotes", "pylint (>=2.11.1)", "pytest (>=7.1.2)", "pytest-mypy", "radon (>=5.1.0)", "toml", "types-setuptools", "types-toml", "vulture"]
|
||||
toml = ["toml (>=0.10.2)"]
|
||||
vulture = ["vulture"]
|
||||
|
||||
[[package]]
|
||||
name = "pytest"
|
||||
version = "7.2.1"
|
||||
description = "pytest: simple powerful testing with Python"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "pytest-7.2.1-py3-none-any.whl", hash = "sha256:c7c6ca206e93355074ae32f7403e8ea12163b1163c976fee7d4d84027c162be5"},
|
||||
{file = "pytest-7.2.1.tar.gz", hash = "sha256:d45e0952f3727241918b8fd0f376f5ff6b301cc0777c6f9a556935c92d8a7d42"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
attrs = ">=19.2.0"
|
||||
colorama = {version = "*", markers = "sys_platform == \"win32\""}
|
||||
exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""}
|
||||
iniconfig = "*"
|
||||
packaging = "*"
|
||||
pluggy = ">=0.12,<2.0"
|
||||
tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""}
|
||||
|
||||
[package.extras]
|
||||
testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"]
|
||||
|
||||
[[package]]
|
||||
name = "pytest-cov"
|
||||
version = "4.0.0"
|
||||
description = "Pytest plugin for measuring coverage."
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
files = [
|
||||
{file = "pytest-cov-4.0.0.tar.gz", hash = "sha256:996b79efde6433cdbd0088872dbc5fb3ed7fe1578b68cdbba634f14bb8dd0470"},
|
||||
{file = "pytest_cov-4.0.0-py3-none-any.whl", hash = "sha256:2feb1b751d66a8bd934e5edfa2e961d11309dc37b73b0eabe73b5945fee20f6b"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
coverage = {version = ">=5.2.1", extras = ["toml"]}
|
||||
pytest = ">=4.6"
|
||||
|
||||
[package.extras]
|
||||
testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"]
|
||||
|
||||
[[package]]
|
||||
name = "python-dateutil"
|
||||
version = "2.8.2"
|
||||
description = "Extensions to the standard Python datetime module"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
|
||||
files = [
|
||||
{file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"},
|
||||
{file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
six = ">=1.5"
|
||||
|
||||
[[package]]
|
||||
name = "pytzdata"
|
||||
version = "2020.1"
|
||||
description = "The Olson timezone database for Python."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
||||
files = [
|
||||
{file = "pytzdata-2020.1-py2.py3-none-any.whl", hash = "sha256:e1e14750bcf95016381e4d472bad004eef710f2d6417240904070b3d6654485f"},
|
||||
{file = "pytzdata-2020.1.tar.gz", hash = "sha256:3efa13b335a00a8de1d345ae41ec78dd11c9f8807f522d39850f2dd828681540"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "six"
|
||||
version = "1.16.0"
|
||||
description = "Python 2 and 3 compatibility utilities"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
|
||||
files = [
|
||||
{file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
|
||||
{file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "snowballstemmer"
|
||||
version = "2.2.0"
|
||||
description = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms."
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
files = [
|
||||
{file = "snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a"},
|
||||
{file = "snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "soupsieve"
|
||||
version = "2.3.2.post1"
|
||||
description = "A modern CSS selector implementation for Beautiful Soup."
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
files = [
|
||||
{file = "soupsieve-2.3.2.post1-py3-none-any.whl", hash = "sha256:3b2503d3c7084a42b1ebd08116e5f81aadfaea95863628c80a3b774a11b7c759"},
|
||||
{file = "soupsieve-2.3.2.post1.tar.gz", hash = "sha256:fc53893b3da2c33de295667a0e19f078c14bf86544af307354de5fcf12a3f30d"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tomli"
|
||||
version = "2.0.1"
|
||||
description = "A lil' TOML parser"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"},
|
||||
{file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
|
||||
]
|
||||
|
||||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = "^3.10"
|
||||
content-hash = "f60d42d4c06c5c2d3a0bcc61f921cb5ee329f4b033d36a95748725268243b01d"
|
31
pyproject.toml
Normal file
31
pyproject.toml
Normal file
|
@ -0,0 +1,31 @@
|
|||
[tool.poetry]
|
||||
name = "tsstats"
|
||||
version = "2.1.0"
|
||||
description = "A simple Teamspeak stats generator"
|
||||
authors = ["Thor77 <thor77@thor77.org>"]
|
||||
license = "MIT"
|
||||
readme = "README.rst"
|
||||
include = ["tsstats/templates/*.jinja2"]
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.10"
|
||||
jinja2 = "^3.1.2"
|
||||
pendulum = "^2.1.2"
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
pytest = "^7.2.0"
|
||||
beautifulsoup4 = "^4.11.1"
|
||||
pylama = "^8.4.1"
|
||||
poethepoet = "^0.18.0"
|
||||
pytest-cov = "^4.0.0"
|
||||
|
||||
[tool.poetry.scripts]
|
||||
tsstats = 'tsstats.__main__:cli'
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core"]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
|
||||
[tool.poe.tasks]
|
||||
test = "pytest --cov=tsstats tsstats"
|
||||
lint = "pylama tsstats"
|
|
@ -1 +0,0 @@
|
|||
Jinja2>=2.8
|
BIN
screenshot.png
BIN
screenshot.png
Binary file not shown.
Before ![]() (image error) Size: 32 KiB After ![]() (image error) Size: 33 KiB ![]() ![]() |
23
setup.py
23
setup.py
|
@ -1,23 +0,0 @@
|
|||
from setuptools import setup
|
||||
|
||||
setup(
|
||||
name='tsstats',
|
||||
version='0.6.6',
|
||||
author='Thor77',
|
||||
author_email='thor77@thor77.org',
|
||||
description='A simple Teamspeak stats-generator',
|
||||
keywords='ts3 teamspeak teamspeak3 tsstats teamspeakstats',
|
||||
url='https://github.com/Thor77/TeamspeakStats',
|
||||
packages=['tsstats'],
|
||||
entry_points={
|
||||
'console_scripts': [
|
||||
'tsstats = tsstats.__main__:cli'
|
||||
]
|
||||
},
|
||||
package_data={
|
||||
'tsstats': ['template.html']
|
||||
},
|
||||
install_requires=[
|
||||
'Jinja2>=2.8'
|
||||
],
|
||||
)
|
|
@ -1,3 +0,0 @@
|
|||
pytest>=2.9.1
|
||||
pyflakes>=1.2.2
|
||||
BeautifulSoup4>=4.4.1
|
|
@ -1,15 +1 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger('tsstats')
|
||||
logger.setLevel(logging.INFO)
|
||||
|
||||
fh = logging.FileHandler('debug.txt', 'w')
|
||||
fh.setLevel(logging.DEBUG)
|
||||
|
||||
ch = logging.StreamHandler()
|
||||
ch.setLevel(logging.INFO)
|
||||
|
||||
logger.addHandler(fh)
|
||||
logger.addHandler(ch)
|
||||
|
|
|
@ -1,21 +1,26 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import logging
|
||||
from os.path import abspath, exists
|
||||
from os.path import join as pathjoin
|
||||
from os.path import abspath, exists, isdir
|
||||
from time import time
|
||||
|
||||
from tsstats.config import parse_config
|
||||
from tsstats import config
|
||||
from tsstats.exceptions import InvalidConfiguration
|
||||
from tsstats.log import parse_logs
|
||||
from tsstats.template import render_template
|
||||
from tsstats.logger import file_handler, stream_handler
|
||||
from tsstats.template import render_servers
|
||||
from tsstats.utils import transform_pretty_identmap
|
||||
|
||||
logger = logging.getLogger('tsstats')
|
||||
|
||||
|
||||
def cli():
|
||||
parser = argparse.ArgumentParser(
|
||||
description='A simple Teamspeak stats-generator - based on server-logs'
|
||||
description='A simple Teamspeak stats-generator,'
|
||||
' based solely on server-logs',
|
||||
argument_default=argparse.SUPPRESS
|
||||
)
|
||||
parser.add_argument(
|
||||
'-c', '--config',
|
||||
|
@ -26,32 +31,67 @@ def cli():
|
|||
)
|
||||
parser.add_argument(
|
||||
'-l', '--log',
|
||||
type=str, help='path to your logfile(s)'
|
||||
type=str, help='path to your logfile(s). '
|
||||
'pass a directory to use all logfiles inside it'
|
||||
)
|
||||
parser.add_argument(
|
||||
'-o', '--output',
|
||||
type=str, help='path to the output-file', default='stats.html'
|
||||
type=str, help='path to the output-file'
|
||||
)
|
||||
parser.add_argument(
|
||||
'-d', '--debug',
|
||||
help='debug mode', action='store_true'
|
||||
)
|
||||
args = parser.parse_args()
|
||||
main(**vars(args))
|
||||
parser.add_argument(
|
||||
'-ds', '--debugstdout',
|
||||
help='write debug output to stdout', action='store_true'
|
||||
)
|
||||
parser.add_argument(
|
||||
'-nod', '--noonlinedc',
|
||||
help='don\'t add connect until now to onlinetime',
|
||||
action='store_false', dest='onlinedc'
|
||||
)
|
||||
parser.add_argument(
|
||||
'-t', '--template',
|
||||
type=str, help='path to custom template'
|
||||
)
|
||||
parser.add_argument(
|
||||
'-dtf', '--datetimeformat',
|
||||
type=str, help='format of date/time-values (datetime.strftime)'
|
||||
)
|
||||
parser.add_argument(
|
||||
'-otth', '--onlinetimethreshold',
|
||||
type=int, help='threshold for displaying onlinetime (in seconds)'
|
||||
)
|
||||
parser.add_argument(
|
||||
'-lsa', '--lastseenabsolute',
|
||||
help='render last seen timestamp absolute (instead of relative)',
|
||||
action='store_false', dest='lastseenrelative'
|
||||
)
|
||||
options = parser.parse_args()
|
||||
if 'config' in options:
|
||||
configuration = config.load(options.config)
|
||||
else:
|
||||
configuration = config.load()
|
||||
for option, value in vars(options).items():
|
||||
configuration.set('General', option, str(value))
|
||||
main(configuration)
|
||||
|
||||
|
||||
def main(config=None, idmap=None, log=None, output=None, debug=False):
|
||||
if debug:
|
||||
def main(configuration):
|
||||
start_time = time()
|
||||
# setup logging
|
||||
if configuration.getboolean('General', 'debug'):
|
||||
logger.setLevel(logging.DEBUG)
|
||||
if configuration.getboolean('General', 'debugstdout'):
|
||||
stream_handler.setLevel(logging.DEBUG)
|
||||
else:
|
||||
logger.addHandler(file_handler)
|
||||
|
||||
if config:
|
||||
config = abspath(config)
|
||||
if not exists(config):
|
||||
logger.fatal('config not found (%s)', config)
|
||||
idmap, log, output, debug = parse_config(config)
|
||||
if debug:
|
||||
logger.setLevel(logging.DEBUG)
|
||||
# attach handlers
|
||||
logger.addHandler(stream_handler)
|
||||
|
||||
idmap = configuration.get('General', 'idmap')
|
||||
if idmap:
|
||||
idmap = abspath(idmap)
|
||||
if not exists(idmap):
|
||||
|
@ -60,12 +100,32 @@ def main(config=None, idmap=None, log=None, output=None, debug=False):
|
|||
identmap = json.load(open(idmap))
|
||||
else:
|
||||
identmap = None
|
||||
if isinstance(identmap, list):
|
||||
identmap = transform_pretty_identmap(identmap)
|
||||
|
||||
if not log or not output:
|
||||
log = configuration.get('General', 'log')
|
||||
if not log:
|
||||
raise InvalidConfiguration('log or output missing')
|
||||
if isdir(log):
|
||||
log = pathjoin(log, '*.log')
|
||||
|
||||
clients = parse_logs(log, ident_map=identmap)
|
||||
render_template(clients, output=abspath(output))
|
||||
servers = parse_logs(
|
||||
log, ident_map=identmap,
|
||||
online_dc=configuration.getboolean('General', 'onlinedc')
|
||||
)
|
||||
render_servers(
|
||||
sorted(servers, key=lambda s: s.sid),
|
||||
output=abspath(configuration.get('General', 'output')),
|
||||
template=configuration.get('General', 'template'),
|
||||
datetime_fmt=configuration.get('General', 'datetimeformat'),
|
||||
onlinetime_threshold=int(configuration.get(
|
||||
'General', 'onlinetimethreshold'
|
||||
)),
|
||||
lastseen_relative=configuration.getboolean(
|
||||
'General', 'lastseenrelative'
|
||||
)
|
||||
)
|
||||
logger.info('Finished after %s seconds', time() - start_time)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
import datetime
|
||||
import logging
|
||||
from collections import MutableMapping
|
||||
|
||||
from tsstats.exceptions import InvalidLog
|
||||
from collections.abc import MutableMapping
|
||||
|
||||
logger = logging.getLogger('tsstats')
|
||||
|
||||
|
@ -24,6 +22,29 @@ class Clients(MutableMapping):
|
|||
self.store = dict()
|
||||
self.update(dict(*args, **kwargs))
|
||||
|
||||
def apply_events(self, events):
|
||||
'''
|
||||
Apply events to this Client-collection
|
||||
|
||||
:param events: list of events to apply
|
||||
:type events: list
|
||||
'''
|
||||
for event in events:
|
||||
# find corresponding client
|
||||
client = self.setdefault(
|
||||
event.identifier,
|
||||
Client(self.ident_map.get(event.identifier, event.identifier))
|
||||
)
|
||||
if event.action == 'set_nick':
|
||||
client.nick = event.arg
|
||||
continue
|
||||
if event.arg_is_client:
|
||||
# if arg is client, replace identifier with Client-obj
|
||||
event = event._replace(
|
||||
arg=self.setdefault(event.arg, Client(event.arg))
|
||||
)
|
||||
client.__getattribute__(event.action)(event.arg)
|
||||
|
||||
def __add__(self, client):
|
||||
'''
|
||||
Add a Client to the collection
|
||||
|
@ -31,14 +52,15 @@ class Clients(MutableMapping):
|
|||
:param client: Client to add to the collection
|
||||
:type id_or_uid: Client
|
||||
'''
|
||||
self.store[client.identifier] = client
|
||||
identifier = client.identifier
|
||||
self.store[self.ident_map.get(identifier, identifier)] = client
|
||||
return self
|
||||
|
||||
def __iter__(self):
|
||||
'''
|
||||
Yield all Client-objects from the collection
|
||||
'''
|
||||
return iter(self.store.values())
|
||||
return iter(self.store.keys())
|
||||
|
||||
def __getitem__(self, key):
|
||||
return self.store[self.ident_map.get(key, key)]
|
||||
|
@ -50,8 +72,10 @@ class Clients(MutableMapping):
|
|||
return len(self.store)
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
key = self.ident_map.get(key, key)
|
||||
self.store[key] = value
|
||||
self.store[self.ident_map.get(key, key)] = value
|
||||
|
||||
def __str__(self):
|
||||
return str(list(map(str, self)))
|
||||
|
||||
|
||||
class Client(object):
|
||||
|
@ -68,17 +92,30 @@ class Client(object):
|
|||
'''
|
||||
# public
|
||||
self.identifier = identifier
|
||||
self.nick = nick
|
||||
self._nick = nick
|
||||
self.nick_history = set()
|
||||
self.connected = 0
|
||||
self.onlinetime = 0
|
||||
self.onlinetime = datetime.timedelta()
|
||||
self.kicks = 0
|
||||
self.pkicks = 0
|
||||
self.bans = 0
|
||||
self.pbans = 0
|
||||
self.last_seen = 0
|
||||
self.last_seen = None
|
||||
# private
|
||||
self._last_connect = 0
|
||||
|
||||
@property
|
||||
def nick(self):
|
||||
return self._nick
|
||||
|
||||
@nick.setter
|
||||
def nick(self, new_nick):
|
||||
if self._nick and new_nick != self._nick:
|
||||
# add old nick to history
|
||||
self.nick_history.add(self._nick)
|
||||
# set new nick
|
||||
self._nick = new_nick
|
||||
|
||||
def connect(self, timestamp):
|
||||
'''
|
||||
Connect client at `timestamp`
|
||||
|
@ -86,7 +123,7 @@ class Client(object):
|
|||
:param timestamp: time of connect
|
||||
:type timestamp: int
|
||||
'''
|
||||
logger.debug('CONNECT %s', self)
|
||||
logger.debug('[%s] CONNECT %s', timestamp, self)
|
||||
self.connected += 1
|
||||
self._last_connect = timestamp
|
||||
|
||||
|
@ -97,12 +134,13 @@ class Client(object):
|
|||
:param timestamp: time of disconnect
|
||||
:type timestamp: int
|
||||
'''
|
||||
logger.debug('DISCONNECT %s', self)
|
||||
logger.debug('[%s] DISCONNECT %s', timestamp, self)
|
||||
if not self.connected:
|
||||
logger.debug('^ disconnect before connect')
|
||||
raise InvalidLog('disconnect before connect!')
|
||||
return
|
||||
self.connected -= 1
|
||||
session_time = timestamp - self._last_connect
|
||||
logger.debug('Session lasted %s', session_time)
|
||||
self.onlinetime += session_time
|
||||
self.last_seen = timestamp
|
||||
|
||||
|
@ -129,7 +167,7 @@ class Client(object):
|
|||
self.bans += 1
|
||||
|
||||
def __str__(self):
|
||||
return '<{},{}>'.format(self.identifier, self.nick)
|
||||
return u'<{}, {}>'.format(self.identifier, self.nick)
|
||||
|
||||
def __getitem__(self, item):
|
||||
return self.__getattribute__(item)
|
||||
def __repr__(self):
|
||||
return self.__str__()
|
||||
|
|
|
@ -1,16 +1,31 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
try:
|
||||
from configparser import ConfigParser
|
||||
from configparser import RawConfigParser
|
||||
except ImportError:
|
||||
from ConfigParser import ConfigParser
|
||||
from ConfigParser import RawConfigParser
|
||||
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger('tsstats')
|
||||
|
||||
|
||||
def parse_config(config_path):
|
||||
DEFAULT_CONFIG = {
|
||||
'General': {
|
||||
'debug': False,
|
||||
'debugstdout': False,
|
||||
'log': '',
|
||||
'output': 'tsstats.html',
|
||||
'idmap': '',
|
||||
'onlinedc': True,
|
||||
'template': 'index.jinja2',
|
||||
'datetimeformat': '%x %X %Z',
|
||||
'onlinetimethreshold': -1,
|
||||
'lastseenrelative': True
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def load(path=None):
|
||||
'''
|
||||
parse config at `config_path`
|
||||
|
||||
|
@ -21,17 +36,14 @@ def parse_config(config_path):
|
|||
:rtype: tuple
|
||||
'''
|
||||
logger.debug('reading config')
|
||||
config = ConfigParser()
|
||||
config.read(config_path)
|
||||
# use dict(ConfigParser.items) to get an easy-to-use interface
|
||||
# compatible with py2 and py3
|
||||
config_items = dict(config.items('General'))
|
||||
if 'debug' in config_items:
|
||||
config_items['debug'] = config.getboolean('General', 'debug')
|
||||
logger.debug('raw config: %s', config_items)
|
||||
return (
|
||||
config_items.get('idmap'),
|
||||
config_items.get('log'),
|
||||
config_items.get('output'),
|
||||
config_items.get('debug', False)
|
||||
)
|
||||
config = RawConfigParser()
|
||||
# use this way to set defaults, because ConfigParser.read_dict
|
||||
# is not available < 3.2
|
||||
for section, items in DEFAULT_CONFIG.items():
|
||||
if section not in config.sections():
|
||||
config.add_section(section)
|
||||
for key, value in items.items():
|
||||
config.set(section, key, str(value))
|
||||
if path:
|
||||
config.read(path)
|
||||
return config
|
||||
|
|
34
tsstats/events.py
Normal file
34
tsstats/events.py
Normal file
|
@ -0,0 +1,34 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from collections import namedtuple
|
||||
|
||||
Event = namedtuple(
|
||||
'Event', ['timestamp', 'identifier', 'action', 'arg', 'arg_is_client']
|
||||
)
|
||||
|
||||
|
||||
def nick(timestamp, identifier, nick):
|
||||
return Event(timestamp, identifier, 'set_nick', nick, arg_is_client=False)
|
||||
|
||||
|
||||
def connect(timestamp, identifier):
|
||||
return Event(
|
||||
timestamp, identifier, 'connect', arg=timestamp, arg_is_client=False
|
||||
)
|
||||
|
||||
|
||||
def disconnect(timestamp, identifier):
|
||||
return Event(
|
||||
timestamp, identifier, 'disconnect', arg=timestamp, arg_is_client=False
|
||||
)
|
||||
|
||||
|
||||
def kick(timestamp, identifier, target_identifier):
|
||||
return Event(
|
||||
timestamp, identifier, 'kick', target_identifier, arg_is_client=True
|
||||
)
|
||||
|
||||
|
||||
def ban(timestamp, identifier, target_identifier):
|
||||
return Event(
|
||||
timestamp, identifier, 'ban', target_identifier, arg_is_client=True
|
||||
)
|
|
@ -1,15 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
|
||||
class InvalidConfiguration(Exception):
|
||||
'''
|
||||
The configuration is invalid (either config-file or cli-args)
|
||||
'''
|
||||
|
||||
|
||||
class InvalidLog(Exception):
|
||||
'''
|
||||
Something impossible appeared at the logs,
|
||||
for example a disconnect before a connect
|
||||
'''
|
||||
pass
|
||||
|
|
234
tsstats/log.py
234
tsstats/log.py
|
@ -1,94 +1,186 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
import itertools
|
||||
import logging
|
||||
import re
|
||||
from datetime import datetime
|
||||
from codecs import open
|
||||
from collections import namedtuple
|
||||
from glob import glob
|
||||
from os.path import basename
|
||||
|
||||
from tsstats.client import Client, Clients
|
||||
import pendulum
|
||||
|
||||
re_log_entry = re.compile('(?P<timestamp>\d{4}-\d\d-\d\d\ \d\d:\d\d:\d\d.\d+)'
|
||||
'\|\ *(?P<level>\w+)\ *\|\ *(?P<component>\w+)\ *'
|
||||
'\|\ *(?P<sid>\d+)\ *\|\ *(?P<message>.*)')
|
||||
re_dis_connect = re.compile(r"'(.*)'\(id:(\d*)\)")
|
||||
from tsstats import events
|
||||
from tsstats.client import Clients
|
||||
|
||||
re_log_filename = re.compile(r'ts3server_(?P<date>\d{4}-\d\d-\d\d)'
|
||||
r'__(?P<time>\d\d_\d\d_\d\d.\d+)_(?P<sid>\d).log')
|
||||
re_log_entry = re.compile(r'(?P<timestamp>\d{4}-\d\d-\d\d\ \d\d:\d\d:\d\d.\d+)'
|
||||
r'\|\ *(?P<level>\w+)\ *\|\ *(?P<component>\w+)\ *'
|
||||
r'\|\ *(?P<sid>\d+)\ *\|\ *(?P<message>.*)')
|
||||
re_dis_connect = re.compile(
|
||||
r"client (?P<action>(dis)?connected) '(?P<nick>.*)'\(id:(?P<clid>\d+)\)")
|
||||
re_disconnect_invoker = re.compile(
|
||||
r'invokername=(.*)\ invokeruid=(.*)\ reasonmsg'
|
||||
)
|
||||
|
||||
log_timestamp_format = '%Y-%m-%d %H:%M:%S.%f'
|
||||
|
||||
TimedLog = namedtuple('TimedLog', ['path', 'timestamp'])
|
||||
Server = namedtuple('Server', ['sid', 'clients'])
|
||||
|
||||
logger = logging.getLogger('tsstats')
|
||||
|
||||
|
||||
def parse_logs(log_glob, ident_map=None):
|
||||
def _bundle_logs(logs):
|
||||
'''
|
||||
parse logs specified by globbing pattern `log_glob`
|
||||
Bundle `logs` by virtualserver-id
|
||||
and sort by timestamp from filename (if exists)
|
||||
|
||||
:param log_glob: path to log-files (supports globbing)
|
||||
:param ident_map: :doc:`identmap`
|
||||
:param logs: list of paths to logfiles
|
||||
:type logs: list
|
||||
|
||||
:return: `logs` bundled by virtualserver-id and sorted by timestamp
|
||||
:rtype: dict{str: [TimedLog]}
|
||||
'''
|
||||
vserver_logfiles = {} # sid: [/path/to/log1, ..., /path/to/logn]
|
||||
for log in logs:
|
||||
# try to get date and sid from filename
|
||||
match = re_log_filename.match(basename(log))
|
||||
if match:
|
||||
match = match.groupdict()
|
||||
timestamp = pendulum.parse('{0} {1}'.format(
|
||||
match['date'], match['time'].replace('_', ':'))
|
||||
)
|
||||
tl = TimedLog(log, timestamp)
|
||||
sid = match['sid']
|
||||
if sid in vserver_logfiles:
|
||||
# if already exists, keep list sorted by timestamp
|
||||
vserver_logfiles[sid].append(tl)
|
||||
vserver_logfiles[sid] =\
|
||||
sorted(vserver_logfiles[sid],
|
||||
key=lambda tl: tl.timestamp)
|
||||
else:
|
||||
# if not exists, just create a list
|
||||
vserver_logfiles[sid] = [tl]
|
||||
else:
|
||||
# fallback to plain sorting
|
||||
vserver_logfiles.setdefault('', [])\
|
||||
.append(TimedLog(log, None))
|
||||
vserver_logfiles[''] =\
|
||||
sorted(vserver_logfiles[''],
|
||||
key=lambda tl: tl.path)
|
||||
return vserver_logfiles
|
||||
|
||||
|
||||
def _parse_line(line):
|
||||
'''
|
||||
Parse events from a single line
|
||||
|
||||
:param line: line to parse events from
|
||||
:type line: str
|
||||
|
||||
:return: parsed events
|
||||
:rtype list
|
||||
'''
|
||||
parsed_events = []
|
||||
match = re_log_entry.match(line)
|
||||
if not match:
|
||||
logger.debug('No match: "%s"', line)
|
||||
return []
|
||||
match = match.groupdict()
|
||||
logdatetime = pendulum.parse(match['timestamp'])
|
||||
message = match['message']
|
||||
if message.startswith('client'):
|
||||
match = re_dis_connect.match(message)
|
||||
if not match:
|
||||
logger.debug('Unsupported client action: "%s"', message)
|
||||
return []
|
||||
nick, clid = match.group('nick'), match.group('clid')
|
||||
|
||||
parsed_events.append(events.nick(logdatetime, clid, nick))
|
||||
|
||||
action = match.group('action')
|
||||
if action == 'connected':
|
||||
parsed_events.append(events.connect(logdatetime, clid))
|
||||
elif action == 'disconnected':
|
||||
parsed_events.append(events.disconnect(logdatetime, clid))
|
||||
if 'invokeruid' in message:
|
||||
re_disconnect_data = re_disconnect_invoker.findall(
|
||||
message)
|
||||
invokernick, invokeruid = re_disconnect_data[0]
|
||||
parsed_events.append(
|
||||
events.nick(logdatetime, invokeruid, invokernick)
|
||||
)
|
||||
if 'bantime' in message:
|
||||
parsed_events.append(
|
||||
events.ban(logdatetime, invokeruid, clid)
|
||||
)
|
||||
else:
|
||||
parsed_events.append(
|
||||
events.kick(logdatetime, invokeruid, clid)
|
||||
)
|
||||
return parsed_events
|
||||
|
||||
|
||||
def parse_logs(log_glob, ident_map=None, online_dc=True):
|
||||
'''
|
||||
Parse logs from `log_glob`
|
||||
|
||||
:param log_glob: path to server-logs (supports globbing)
|
||||
:param ident_map: identmap used for Client-initializations
|
||||
|
||||
:type log_glob: str
|
||||
:type ident_map: dict
|
||||
|
||||
:return: parsed clients
|
||||
:rtype: tsstats.client.Clients
|
||||
:return: list of servers
|
||||
:rtype: [tsstats.log.Server]
|
||||
'''
|
||||
clients = Clients(ident_map)
|
||||
for log_file in sorted(log_file for log_file in glob(log_glob)):
|
||||
clients = parse_log(log_file, ident_map, clients)
|
||||
return clients
|
||||
|
||||
|
||||
def parse_log(log_path, ident_map=None, clients=None):
|
||||
'''
|
||||
parse log-file at `log_path`
|
||||
|
||||
:param log_path: path to log-file
|
||||
:param ident_map: :doc:`identmap`
|
||||
:param clients: clients-object to add parsing-results to
|
||||
|
||||
:type log_path: str
|
||||
:type ident_map: dict
|
||||
:type clients: tsstats.client.Clients
|
||||
|
||||
:return: parsed clients
|
||||
:rtype: tsstats.client.Clients
|
||||
'''
|
||||
if not clients:
|
||||
for virtualserver_id, logs in _bundle_logs(glob(log_glob)).items():
|
||||
clients = Clients(ident_map)
|
||||
log_file = open(log_path)
|
||||
# process lines
|
||||
logger.debug('Started parsing of %s', log_file.name)
|
||||
for line in log_file:
|
||||
match = re_log_entry.match(line)
|
||||
if not match:
|
||||
logger.debug('No match: "%s"', line)
|
||||
continue
|
||||
match = match.groupdict()
|
||||
stripped_time = datetime.strptime(match['timestamp'],
|
||||
log_timestamp_format)
|
||||
logdatetime = int((stripped_time - datetime(1970, 1, 1))
|
||||
.total_seconds())
|
||||
message = match['message']
|
||||
if message.startswith('client'):
|
||||
nick, clid = re_dis_connect.findall(message)[0]
|
||||
client = clients.setdefault(clid, Client(clid, nick))
|
||||
client.nick = nick # set nick to display changes
|
||||
if message.startswith('client connected'):
|
||||
client.connect(logdatetime)
|
||||
elif message.startswith('client disconnected'):
|
||||
client.disconnect(logdatetime)
|
||||
if 'invokeruid' in message:
|
||||
re_disconnect_data = re_disconnect_invoker.findall(
|
||||
message)
|
||||
invokernick, invokeruid = re_disconnect_data[0]
|
||||
invoker = clients.setdefault(invokeruid,
|
||||
Client(invokeruid))
|
||||
invoker.nick = invokernick
|
||||
if 'bantime' in message:
|
||||
invoker.ban(client)
|
||||
for index, log in enumerate(logs):
|
||||
with open(log.path, encoding='utf-8') as f:
|
||||
logger.debug('Started parsing of %s', f.name)
|
||||
# parse logfile line by line and filter lines without events
|
||||
events = filter(None, map(_parse_line, f))
|
||||
all_events = list(itertools.chain.from_iterable(events))
|
||||
# chain apply events to Client-obj
|
||||
clients.apply_events(all_events)
|
||||
|
||||
# find connected clients
|
||||
online_clients = list(
|
||||
filter(lambda c: c.connected, clients.values())
|
||||
)
|
||||
|
||||
if online_clients:
|
||||
logger.debug(
|
||||
'Some clients are still connected: %s', online_clients
|
||||
)
|
||||
if index == len(logs) - 1:
|
||||
if online_dc:
|
||||
logger.debug(
|
||||
'Last log => disconnecting online clients'
|
||||
)
|
||||
# last iteration
|
||||
# => disconnect online clients if desired
|
||||
for online_client in online_clients:
|
||||
online_client.disconnect(pendulum.now('UTC'))
|
||||
online_client.connected += 1
|
||||
else:
|
||||
invoker.kick(client)
|
||||
logger.debug('Finished parsing of %s', log_file.name)
|
||||
return clients
|
||||
logger.warn(
|
||||
'Server didn\'t disconnect all clients on shutdown'
|
||||
' or logfile is incorrectly named/corrupted (%s).'
|
||||
' Check debuglog for details',
|
||||
f.name
|
||||
)
|
||||
logger.debug(
|
||||
'Will handle this by disconnecting all clients on'
|
||||
' last event timestamp'
|
||||
)
|
||||
last_event_timestamp = all_events[-1].timestamp
|
||||
logger.debug(
|
||||
'Last event timestamp: %s', last_event_timestamp)
|
||||
for online_client in online_clients:
|
||||
online_client.disconnect(last_event_timestamp)
|
||||
|
||||
logger.debug('Finished parsing of %s', f.name)
|
||||
if len(clients) >= 1:
|
||||
# assemble Server-obj and yield
|
||||
yield Server(virtualserver_id, clients)
|
||||
|
|
13
tsstats/logger.py
Normal file
13
tsstats/logger.py
Normal file
|
@ -0,0 +1,13 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import logging
|
||||
|
||||
# setup logger
|
||||
logger = logging.getLogger('tsstats')
|
||||
logger.setLevel(logging.INFO)
|
||||
|
||||
# define handlers
|
||||
file_handler = logging.FileHandler('debug.txt', 'w', delay=True)
|
||||
file_handler.setLevel(logging.DEBUG)
|
||||
|
||||
stream_handler = logging.StreamHandler()
|
||||
stream_handler.setLevel(logging.INFO)
|
|
@ -1,37 +0,0 @@
|
|||
<html>
|
||||
<head>
|
||||
<title>{{ title }}</title>
|
||||
<meta charset="utf-8">
|
||||
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-1q8mTJOASx8j1Au+a5WDVnPi2lkFfwwEAa8hDDdjZlpLegxhjVME1fgjWPGmkzs7" crossorigin="anonymous">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/hint.css/2.3.1/hint.min.css" rel="stylesheet" integrity="sha256-8TwJSTMkVAXH6Dfm206gsmzucikwLtBVQjN8qVVHRJA=" crossorigin="anonymous">
|
||||
<style type="text/css">
|
||||
h1, p {
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container-fluid">
|
||||
{% if debug %}
|
||||
<div class="alert alert-danger" role="alert" style="text-align: center;">
|
||||
<b>DEBUG</b>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% for headline, list in objs %}
|
||||
{% if list|length > 0 %}
|
||||
{% set headline_id = headline|lower|replace(' ', '_') %}
|
||||
<h1>{{ headline }}</h2>
|
||||
<ul class="list-group" id="{{ headline_id }}">
|
||||
{% for client, value in list %}
|
||||
{% set id = headline_id + "." + client.nick %}
|
||||
<li id="{{ id }}" onclick="window.location = '#{{ id }}'" class="list-group-item{{ ' list-group-item-success' if client.connected else loop.cycle('" style="background-color: #eee;', '') }}">
|
||||
<span {% if not client.connected %}class="hint--right" data-hint="{{ client.last_seen|frmttime }}"{% endif %}>{{ client.nick }}{{ " (" + client.identifier + ")" if debug }}</span>
|
||||
<span class="badge">{{ value }}</span>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
|
@ -1,56 +1,110 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
import logging
|
||||
from os.path import dirname
|
||||
from time import localtime, strftime
|
||||
from collections import namedtuple
|
||||
from os.path import dirname, join
|
||||
|
||||
import pendulum
|
||||
from jinja2 import ChoiceLoader, Environment, FileSystemLoader, PackageLoader
|
||||
|
||||
from tsstats.utils import seconds_to_text, sort_clients
|
||||
from tsstats.log import Server
|
||||
from tsstats.utils import filter_threshold, seconds_to_text, sort_clients
|
||||
|
||||
logger = logging.getLogger('tsstats')
|
||||
|
||||
SortedClients = namedtuple('SortedClients', [
|
||||
'onlinetime', 'kicks', 'pkicks', 'bans', 'pbans'])
|
||||
|
||||
def render_template(clients, output, title='TeamspeakStats'):
|
||||
|
||||
def prepare_clients(clients, onlinetime_threshold=-1):
|
||||
'''
|
||||
render template with `clients`
|
||||
Prepare `clients` for rendering
|
||||
|
||||
:param clients: clients to fill template with
|
||||
sort them, clean their nick-history and convert onlinetime to string
|
||||
|
||||
:param clients: List of clients to prepare
|
||||
:param onlinetime_threshold: threshold for clients onlinetime
|
||||
|
||||
:type clients: tsstats.client.Clients
|
||||
:type onlinetime_treshold: int
|
||||
|
||||
:return: `clients` sorted by onlinetime, kics, pkicks, bans and pbans
|
||||
:rtype: tsstats.template.SortedClients
|
||||
'''
|
||||
# sort by onlinetime
|
||||
onlinetime_ = sort_clients(
|
||||
clients, lambda c: c.onlinetime.total_seconds()
|
||||
)
|
||||
# filter clients not matching threshold
|
||||
onlinetime_ = filter_threshold(onlinetime_, onlinetime_threshold)
|
||||
# convert timespans to text
|
||||
onlinetime = [
|
||||
(client, seconds_to_text(int(onlinetime)))
|
||||
for client, onlinetime in onlinetime_
|
||||
]
|
||||
return SortedClients(
|
||||
onlinetime=onlinetime,
|
||||
kicks=sort_clients(clients, lambda c: c.kicks),
|
||||
pkicks=sort_clients(clients, lambda c: c.pkicks),
|
||||
bans=sort_clients(clients, lambda c: c.bans),
|
||||
pbans=sort_clients(clients, lambda c: c.pbans)
|
||||
)
|
||||
|
||||
|
||||
def render_servers(servers, output, title='TeamspeakStats',
|
||||
template='index.jinja2', datetime_fmt='%x %X %Z',
|
||||
onlinetime_threshold=-1, lastseen_relative=True):
|
||||
'''
|
||||
Render `servers`
|
||||
|
||||
:param servers: list of servers to render
|
||||
:param output: path to output-file
|
||||
:param template_name: path to template-file
|
||||
:param title: title of the resulting html-document
|
||||
:param template_path: path to template-file
|
||||
:param datetime_fmt: custom datetime-format
|
||||
:param onlinetime_threshold: threshold for clients onlinetime
|
||||
:param lastseen_relative: render last seen timestamp relative
|
||||
|
||||
:type clients: tsstats.client.Clients
|
||||
|
||||
:type servers: [tsstats.log.Server]
|
||||
:type output: str
|
||||
:type template_name: str
|
||||
:type title: str
|
||||
:type template_path: str
|
||||
:type datetime_fmt: str
|
||||
:type onlinetime_threshold: int
|
||||
:type lastseen_relative: bool
|
||||
'''
|
||||
# prepare clients
|
||||
clients_onlinetime_ = sort_clients(clients, 'onlinetime')
|
||||
clients_onlinetime = [
|
||||
(client, seconds_to_text(onlinetime))
|
||||
for client, onlinetime in clients_onlinetime_
|
||||
# preparse servers
|
||||
prepared_servers = [
|
||||
Server(sid, prepare_clients(clients, onlinetime_threshold))
|
||||
for sid, clients in servers
|
||||
]
|
||||
|
||||
clients_kicks = sort_clients(clients, 'kicks')
|
||||
clients_pkicks = sort_clients(clients, 'pkicks')
|
||||
clients_bans = sort_clients(clients, 'bans')
|
||||
clients_pbans = sort_clients(clients, 'pbans')
|
||||
objs = [('Onlinetime', clients_onlinetime), ('Kicks', clients_kicks),
|
||||
('passive Kicks', clients_pkicks),
|
||||
('Bans', clients_bans), ('passive Bans', clients_pbans)]
|
||||
|
||||
# render
|
||||
template_loader = ChoiceLoader([
|
||||
PackageLoader(__package__, ''),
|
||||
FileSystemLoader(dirname(__file__))
|
||||
PackageLoader(__package__, 'templates'),
|
||||
FileSystemLoader(join(dirname(__file__), 'templates'))
|
||||
])
|
||||
template_env = Environment(loader=template_loader)
|
||||
|
||||
def fmttime(timestamp):
|
||||
return strftime('%x %X', localtime(int(timestamp)))
|
||||
template_env.filters['frmttime'] = fmttime
|
||||
template = template_env.get_template('template.html')
|
||||
with open(output, 'w') as f:
|
||||
f.write(template.render(title=title, objs=objs,
|
||||
debug=logger.level <= logging.DEBUG))
|
||||
def frmttime(timestamp):
|
||||
if not timestamp:
|
||||
return ''
|
||||
formatted = timestamp.strftime(datetime_fmt)
|
||||
logger.debug('Formatting timestamp %s -> %s', timestamp, formatted)
|
||||
return formatted
|
||||
|
||||
def lastseen(timestamp):
|
||||
if lastseen_relative:
|
||||
return timestamp.diff_for_humans()
|
||||
else:
|
||||
return frmttime(timestamp)
|
||||
template_env.filters['frmttime'] = frmttime
|
||||
template_env.filters['lastseen'] = lastseen
|
||||
template = template_env.get_template(template)
|
||||
logger.debug('Rendering template %s', template)
|
||||
template.stream(title=title, servers=prepared_servers,
|
||||
debug=logger.level <= logging.DEBUG,
|
||||
creation_time=pendulum.now('UTC'))\
|
||||
.dump(output, encoding='utf-8')
|
||||
logger.debug('Wrote rendered template to %s', output)
|
||||
|
|
79
tsstats/templates/index.jinja2
Normal file
79
tsstats/templates/index.jinja2
Normal file
|
@ -0,0 +1,79 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>{{ title }}</title>
|
||||
<meta charset="utf-8">
|
||||
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/hint.css@2.6.0/hint.min.css" integrity="sha256-UMhOZKeAbUSd/AoZKm+rlqzsBhzI7dTOYf2Euns4Es8=" crossorigin="anonymous">
|
||||
<meta name="referrer" content="no-referrer">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<style type="text/css">
|
||||
a {
|
||||
color: inherit;
|
||||
}
|
||||
a:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
.navbar-toggler {
|
||||
cursor: pointer;
|
||||
}
|
||||
@media screen and (max-width: 767px) {
|
||||
.hint--medium--xs:after {
|
||||
white-space: normal;
|
||||
line-height: 1.4em;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<nav class="navbar navbar-expand-md navbar-light bg-light sticky-top">
|
||||
<a href="" class="navbar-brand">{{ title }}</a>
|
||||
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="main-nav" aria-controls="main-nav" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
|
||||
<div id="main-nav" class="collapse navbar-collapse">
|
||||
<ul class="navbar-nav mr-auto">
|
||||
{% for sid, _ in servers %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="#sid{{ sid }}">Server {{ sid }}</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% if debug %}
|
||||
<span class="navbar-text" style="color: red">debug mode</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="container-fluid">
|
||||
{% for server in servers %}
|
||||
<h1 class="display-4" id="sid{{ server.sid }}">
|
||||
<a href="#sid{{ server.sid }}">Server {{ server.sid }}</a>
|
||||
</h1>
|
||||
{% include 'stats.jinja2' %}
|
||||
{% endfor %}
|
||||
<small>Generated by <a href="https://github.com/Thor77/TeamspeakStats" rel="noopener">TeamspeakStats</a> at {{ creation_time|frmttime }}</small>
|
||||
</div>
|
||||
|
||||
<script type="text/javascript">
|
||||
(function() {
|
||||
var collapseTogglers = document.querySelectorAll('[data-toggle="collapse"]');
|
||||
var toggleTarget;
|
||||
Array.prototype.forEach.call(collapseTogglers, function(toggler) {
|
||||
toggler.addEventListener('click', function(event) {
|
||||
toggleTarget = document.getElementById(toggler.dataset.target);
|
||||
if (toggler.getAttribute('aria-expanded') === 'true') {
|
||||
toggleTarget.classList.add('collapse');
|
||||
toggler.setAttribute('aria-expanded', false);
|
||||
} else {
|
||||
toggleTarget.classList.remove('collapse');
|
||||
toggler.setAttribute('aria-expanded', true);
|
||||
}
|
||||
});
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
22
tsstats/templates/stats.jinja2
Normal file
22
tsstats/templates/stats.jinja2
Normal file
|
@ -0,0 +1,22 @@
|
|||
{% set clients = server.clients %}
|
||||
{% for headline, clients in [
|
||||
('Onlinetime', clients.onlinetime),
|
||||
('Kicks', clients.kicks),
|
||||
('Passive kicks', clients.pkicks),
|
||||
('Bans', clients.bans),
|
||||
('Passive bans', clients.pbans)
|
||||
] %}
|
||||
{% if clients|length > 0 %}
|
||||
{% set headline_id = [server.sid, headline|lower|replace(' ', '_')]|join('.') %}
|
||||
<h2><a href="#{{ headline_id }}">{{ headline }}</a></h2>
|
||||
<ul class="list-group my-3" id="{{ headline_id }}">
|
||||
{% for client, value in clients %}
|
||||
{% set id = [headline_id, client.nick|striptags]|join('.') %}
|
||||
<li id="{{ id }}" class="list-group-item d-flex justify-content-between align-items-center{{ ' list-group-item-success' if client.connected else loop.cycle('" style="background-color: #eee;', '') }}">
|
||||
<span class="hint--right hint--medium--xs hint--no-shadow" data-hint="{{ client.nick_history|join(', ') }}"><a href="#{{ id }}">{{ client.nick }}{{ " (" + client.identifier + ")" if debug }}</a></span>
|
||||
<span class="badge badge-secondary badge-pill"><div{% if not client.connected and headline == 'Onlinetime' %} class="hint--left hint--no-shadow" data-hint="{{ client.last_seen|lastseen }}"{% endif %}>{{ value }}</div></span>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% endfor %}
|
|
@ -0,0 +1 @@
|
|||
# -*- coding: utf-8 -*-
|
14
tsstats/tests/conftest.py
Normal file
14
tsstats/tests/conftest.py
Normal file
|
@ -0,0 +1,14 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from os import remove
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def output(request):
|
||||
output_path = 'tsstats/tests/res/output.html'
|
||||
|
||||
def clean():
|
||||
remove(output_path)
|
||||
request.addfinalizer(clean)
|
||||
yield output_path
|
3
tsstats/tests/res/test.log.groups
Normal file
3
tsstats/tests/res/test.log.groups
Normal file
|
@ -0,0 +1,3 @@
|
|||
2016-03-06 18:34:52.674929|INFO |VirtualServer |1 |client 'Client1'(id:1) was added to channelgroup 'Channelgroup1'(id:1) by client 'Client2'(id:2) in channel 'Channel1'(id:1)
|
||||
2016-02-20 11:11:28.451329|INFO |VirtualServer |1 |client (id:1) was added to servergroup 'Servergroup1'(id:1) by client 'Client2'(id:2)
|
||||
2016-02-19 14:35:00.253289|INFO |VirtualServer |1 |client (id:1) was removed from servergroup 'Servergroup1'(id:1) by client 'Client2'(id:2)
|
7
tsstats/tests/res/test.log.identmap_wrong_identifier
Normal file
7
tsstats/tests/res/test.log.identmap_wrong_identifier
Normal file
|
@ -0,0 +1,7 @@
|
|||
2017-03-28 01:00:00.000000|INFO |VirtualServer| 1| listening on 0.0.0.0:9987
|
||||
2017-03-28 03:00:00.000000|INFO |VirtualServerBase| 2| client connected 'Client1'(id:2) from 1.2.3.4:1234
|
||||
2017-03-28 04:00:00.000000|INFO |VirtualServerBase| 2| client disconnected 'Client1'(id:2) reason 'reasonmsg=ByeBye!'
|
||||
2017-03-28 01:00:00.000000|INFO |VirtualServerBase| 2| client connected 'Client1'(id:1) from 1.2.3.4:1234
|
||||
2017-03-28 02:00:00.000000|INFO |VirtualServerBase| 2| client disconnected 'Client1'(id:1) reason 'reasonmsg=ByeBye!'
|
||||
2017-03-28 05:00:00.000000|INFO |VirtualServerBase| 2| client connected 'Client1'(id:3) from 1.2.3.4:1234
|
||||
2017-03-28 06:00:00.000000|INFO |VirtualServerBase| 2| client disconnected 'Client1'(id:3) reason 'reasonmsg=ByeBye!'
|
4
tsstats/tests/res/test.log.stopped
Normal file
4
tsstats/tests/res/test.log.stopped
Normal file
|
@ -0,0 +1,4 @@
|
|||
2015-05-18 15:00:00.000000|INFO |VirtualServer| 1| listening on 0.0.0.0:9987
|
||||
2015-05-18 15:30:00.000000|INFO |VirtualServerBase| 3| client connected 'Client1'(id:1) from 1.2.3.4:1234
|
||||
2015-05-18 15:40:00.000000|INFO |VirtualServerBase| 3| client connected 'Client2'(id:2) from 5.6.7.8:5678
|
||||
2015-05-18 15:50:00.000000|INFO |VirtualServerBase| 1| stopped
|
|
@ -1,2 +1,2 @@
|
|||
2015-05-18 16:00:14.951191|INFO |VirtualServerBase| 3| client disconnected 'Client1'(id:1) reason 'reasonmsg=ByeBye!'
|
||||
2015-05-18 15:55:23.456679|INFO |VirtualServerBase| 3| client connected 'Client1'(id:1) from 1.2.3.4:1234
|
||||
2015-05-18 15:55:23.456679|INFO |VirtualServerBase| 3| client connected 'Cläönt1'(id:1) from 1.2.3.4:1234
|
||||
2015-05-18 16:00:14.951191|INFO |VirtualServerBase| 3| client disconnected 'Cläönt1'(id:1) reason 'reasonmsg=ByeBye!'
|
|
@ -1,19 +1,25 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import pytest
|
||||
|
||||
from tsstats.client import Client, Clients
|
||||
|
||||
clients = Clients()
|
||||
cl1 = Client('1')
|
||||
cl2 = Client('2')
|
||||
clients += cl1
|
||||
clients += cl2
|
||||
uidcl1 = Client('UID1')
|
||||
uidcl2 = Client('UID2')
|
||||
clients += uidcl1
|
||||
clients += uidcl2
|
||||
|
||||
@pytest.fixture(scope='module')
|
||||
def clients():
|
||||
clients = Clients()
|
||||
cl1 = Client('1')
|
||||
cl2 = Client('2')
|
||||
clients += cl1
|
||||
clients += cl2
|
||||
uidcl1 = Client('UID1')
|
||||
uidcl2 = Client('UID2')
|
||||
clients += uidcl1
|
||||
clients += uidcl2
|
||||
return (clients, cl1, cl2, uidcl1, uidcl2)
|
||||
|
||||
|
||||
def test_client_get():
|
||||
def test_client_get(clients):
|
||||
clients, cl1, cl2, uidcl1, uidcl2 = clients
|
||||
assert clients['1'] == cl1
|
||||
assert clients['2'] == cl2
|
||||
assert clients['UID1'] == uidcl1
|
||||
|
@ -23,21 +29,37 @@ def test_client_get():
|
|||
clients['UID3']
|
||||
|
||||
|
||||
def test_client_repr():
|
||||
assert str(clients['1']) == '<1,None>'
|
||||
assert str(clients['2']) == '<2,None>'
|
||||
assert str(clients['UID1']) == '<UID1,None>'
|
||||
assert str(clients['UID2']) == '<UID2,None>'
|
||||
def test_client_repr(clients):
|
||||
clients, _, _, _, _ = clients
|
||||
assert str(clients['1']) == '<1, None>'
|
||||
assert str(clients['2']) == '<2, None>'
|
||||
assert str(clients['UID1']) == '<UID1, None>'
|
||||
assert str(clients['UID2']) == '<UID2, None>'
|
||||
assert repr(clients['1']) == str(clients['1'])
|
||||
|
||||
|
||||
def test_clients_iter():
|
||||
client_list = list(iter(clients))
|
||||
def test_client_nick(clients):
|
||||
_, cl1, _, _, _ = clients
|
||||
assert cl1.nick is None
|
||||
assert not cl1.nick_history
|
||||
cl1.nick = 'Client1'
|
||||
assert cl1.nick == 'Client1'
|
||||
assert None not in cl1.nick_history
|
||||
cl1.nick = 'NewClient1'
|
||||
assert cl1.nick == 'NewClient1'
|
||||
assert 'Client1' in cl1.nick_history
|
||||
|
||||
|
||||
def test_clients_iter(clients):
|
||||
clients, cl1, cl2, uidcl1, uidcl2 = clients
|
||||
client_list = list(clients.values())
|
||||
assert cl1 in client_list
|
||||
assert cl2 in client_list
|
||||
assert uidcl1 in client_list
|
||||
assert uidcl2 in client_list
|
||||
|
||||
|
||||
def test_clients_delete():
|
||||
def test_clients_delete(clients):
|
||||
clients, cl1, _, _, _ = clients
|
||||
del clients['1']
|
||||
assert cl1 not in clients
|
||||
assert '1' not in clients
|
||||
|
|
|
@ -1,44 +1,30 @@
|
|||
try:
|
||||
from configparser import ConfigParser
|
||||
except ImportError:
|
||||
from ConfigParser import ConfigParser
|
||||
|
||||
from os import remove
|
||||
from os.path import abspath, exists
|
||||
|
||||
# -*- coding: utf-8 -*-
|
||||
import pytest
|
||||
|
||||
from tsstats.config import parse_config
|
||||
|
||||
configpath = abspath('tsstats/tests/res/test.cfg')
|
||||
|
||||
|
||||
def create_config(values, key='General'):
|
||||
config = ConfigParser()
|
||||
config.add_section('General')
|
||||
for option, value in values.items():
|
||||
config.set('General', option, value)
|
||||
with open(configpath, 'w') as configfile:
|
||||
config.write(configfile)
|
||||
from tsstats.config import load
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def config(request):
|
||||
def clean():
|
||||
if exists(configpath):
|
||||
remove(configpath)
|
||||
request.addfinalizer(clean)
|
||||
def config():
|
||||
return load()
|
||||
|
||||
|
||||
def test_config(config):
|
||||
create_config({
|
||||
'idmap': 'tsstats/tests/res/id_map.json',
|
||||
'log': 'tsstats/tests/res/test.log',
|
||||
'output': 'output.html',
|
||||
'debug': 'true'
|
||||
})
|
||||
idmap, log, output, debug = parse_config(configpath)
|
||||
assert idmap == 'tsstats/tests/res/id_map.json'
|
||||
assert log == 'tsstats/tests/res/test.log'
|
||||
assert output == 'output.html'
|
||||
assert debug is True
|
||||
assert not config.getboolean('General', 'debug')
|
||||
assert config.getboolean('General', 'onlinedc')
|
||||
config.set('General', 'idmap', 'tsstats/tests/res/id_map.json')
|
||||
assert config.get('General', 'idmap') ==\
|
||||
'tsstats/tests/res/id_map.json'
|
||||
config.set('General', 'log', 'tsstats/tests/res/test.log')
|
||||
assert config.get('General', 'log') == 'tsstats/tests/res/test.log'
|
||||
config.set('General', 'output', 'output.html')
|
||||
assert config.get('General', 'output') == 'output.html'
|
||||
|
||||
|
||||
def test_read():
|
||||
config = load(path='tsstats/tests/res/config.ini')
|
||||
# test defaults
|
||||
assert not config.getboolean('General', 'debug')
|
||||
# test written values
|
||||
assert config.get('General', 'log') == 'tsstats/tests/res/test.log'
|
||||
assert config.get('General', 'output') == 'tsstats/tests/res/output.html'
|
||||
|
|
|
@ -1,20 +1,69 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import pytest
|
||||
|
||||
from tsstats.client import Client, Clients
|
||||
|
||||
ident_map = {
|
||||
'1': '2',
|
||||
'5': '2',
|
||||
'UID1': 'UID2',
|
||||
'UID5': 'UID2'
|
||||
}
|
||||
clients = Clients(ident_map)
|
||||
cl = Client('2', 'Client2')
|
||||
uidcl = Client('UID2', 'Client2++')
|
||||
clients += cl
|
||||
clients += uidcl
|
||||
from tsstats.log import parse_logs
|
||||
from tsstats.utils import transform_pretty_identmap
|
||||
|
||||
|
||||
def test_ident_map():
|
||||
@pytest.fixture(scope='module')
|
||||
def identmap_clients():
|
||||
clients = Clients({
|
||||
'1': '2',
|
||||
'5': '2',
|
||||
'UID1': 'UID2',
|
||||
'UID5': 'UID2'
|
||||
})
|
||||
cl = Client('2', 'Client2')
|
||||
uidcl = Client('UID2', 'Client2++')
|
||||
clients += cl
|
||||
clients += uidcl
|
||||
return (clients, cl, uidcl)
|
||||
|
||||
|
||||
def test_ident_map(identmap_clients):
|
||||
clients, cl, uidcl = identmap_clients
|
||||
assert clients['1'] == cl
|
||||
assert clients['5'] == cl
|
||||
assert clients['UID1'] == uidcl
|
||||
assert clients['UID5'] == uidcl
|
||||
|
||||
|
||||
@pytest.mark.parametrize('test_input,expected', [
|
||||
(
|
||||
[
|
||||
{'primary_id': '1', 'alternate_ids': ['3', '6']},
|
||||
{'primary_id': '4', 'alternate_ids': ['9', '42', '23']}
|
||||
],
|
||||
(('3', '1'), ('6', '1'), ('9', '4'), ('42', '4'), ('23', '4'))
|
||||
),
|
||||
(
|
||||
[
|
||||
{'name': 'Friend 1', 'primary_id': '2', 'alternate_ids': ['4']},
|
||||
{
|
||||
'name': 'Friend 3',
|
||||
'primary_id': '8',
|
||||
'alternate_ids': ['9', '14']
|
||||
}
|
||||
],
|
||||
(('4', '2'), ('9', '8'), ('14', '8'))
|
||||
)
|
||||
])
|
||||
def test_transform_pretty_identmap(test_input, expected):
|
||||
transformed_identmap = transform_pretty_identmap(test_input)
|
||||
for alternate, primary in expected:
|
||||
assert transformed_identmap[alternate] == primary
|
||||
|
||||
|
||||
def test_ident_map_wrong_identifier():
|
||||
clients = list(parse_logs(
|
||||
'tsstats/tests/res/test.log.identmap_wrong_identifier', ident_map={
|
||||
'2': '1',
|
||||
'3': '1'
|
||||
}
|
||||
))[0].clients
|
||||
client = clients.get('1')
|
||||
# assert client exists
|
||||
assert client
|
||||
# assert correct identifier
|
||||
assert client.identifier == '1'
|
||||
|
|
|
@ -1,12 +1,19 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import pendulum
|
||||
import pytest
|
||||
|
||||
from tsstats.exceptions import InvalidLog
|
||||
from tsstats.log import parse_log, parse_logs
|
||||
from tsstats import events
|
||||
from tsstats.log import TimedLog, _bundle_logs, _parse_line, parse_logs
|
||||
from tsstats.template import render_servers
|
||||
|
||||
testlog_path = 'tsstats/tests/res/test.log'
|
||||
|
||||
static_timestamp = pendulum.datetime(2015, 5, 18, 15, 52, 52, 685612)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def clients():
|
||||
return parse_log('tsstats/tests/res/test.log')
|
||||
return list(parse_logs(testlog_path, online_dc=False))[0].clients
|
||||
|
||||
|
||||
def test_log_client_count(clients):
|
||||
|
@ -14,8 +21,10 @@ def test_log_client_count(clients):
|
|||
|
||||
|
||||
def test_log_onlinetime(clients):
|
||||
assert clients['1'].onlinetime == 402
|
||||
assert clients['2'].onlinetime == 20
|
||||
assert clients['1'].onlinetime == pendulum.duration(
|
||||
seconds=402, microseconds=149208)
|
||||
assert clients['2'].onlinetime == pendulum.duration(
|
||||
seconds=19, microseconds=759644)
|
||||
|
||||
|
||||
def test_log_kicks(clients):
|
||||
|
@ -34,11 +43,102 @@ def test_log_pbans(clients):
|
|||
assert clients['2'].pbans == 1
|
||||
|
||||
|
||||
def test_log_invalid():
|
||||
with pytest.raises(InvalidLog):
|
||||
parse_log('tsstats/tests/res/test.log.broken')
|
||||
@pytest.mark.parametrize("logs,bundled", [
|
||||
(
|
||||
['l1.log', 'l2.log'],
|
||||
{'': [TimedLog('l1.log', None), TimedLog('l2.log', None)]}
|
||||
),
|
||||
(
|
||||
[
|
||||
'ts3server_2016-06-06__14_22_09.527229_1.log',
|
||||
'ts3server_2017-07-07__15_23_10.638340_1.log'
|
||||
],
|
||||
{
|
||||
'1': [
|
||||
TimedLog(
|
||||
'ts3server_2016-06-06__14_22_09.527229_1.log',
|
||||
pendulum.datetime(
|
||||
year=2016, month=6, day=6, hour=14, minute=22,
|
||||
second=9, microsecond=527229
|
||||
)
|
||||
),
|
||||
TimedLog(
|
||||
'ts3server_2017-07-07__15_23_10.638340_1.log',
|
||||
pendulum.datetime(
|
||||
year=2017, month=7, day=7, hour=15, minute=23,
|
||||
second=10, microsecond=638340
|
||||
)
|
||||
)
|
||||
]
|
||||
}
|
||||
)
|
||||
])
|
||||
def test_log_bundle(logs, bundled):
|
||||
assert _bundle_logs(logs) == bundled
|
||||
|
||||
|
||||
def test_log_multiple():
|
||||
assert len(parse_log('tsstats/tests/res/test.log')) == \
|
||||
len(parse_logs('tsstats/tests/res/test.log'))
|
||||
def test_log_client_online():
|
||||
current_time = pendulum.now()
|
||||
|
||||
pendulum.set_test_now(current_time)
|
||||
clients = list(parse_logs(testlog_path))[0].clients
|
||||
old_onlinetime = int(clients['1'].onlinetime.total_seconds())
|
||||
|
||||
pendulum.set_test_now(current_time.add(seconds=2)) # add 2s to .now()
|
||||
clients = list(parse_logs(testlog_path))[0].clients
|
||||
assert int(clients['1'].onlinetime.total_seconds()) == old_onlinetime + 2
|
||||
|
||||
|
||||
def test_parse_groups():
|
||||
server = list(parse_logs('tsstats/tests/res/test.log.groups'))
|
||||
assert len(server) == 0
|
||||
|
||||
|
||||
def test_parse_utf8(output):
|
||||
servers = parse_logs(testlog_path + '.utf8')
|
||||
render_servers(servers, output)
|
||||
|
||||
|
||||
def test_parse_invalid_line():
|
||||
assert _parse_line('INVALID') == []
|
||||
|
||||
|
||||
@pytest.mark.parametrize('line,expected_events', [
|
||||
(
|
||||
"client connected 'Client1'(id:1) from 1.2.3.4:1234",
|
||||
[
|
||||
events.connect(static_timestamp, '1')
|
||||
]
|
||||
),
|
||||
(
|
||||
"client disconnected 'Client1'(id:1) reason 'reasonmsg=ByeBye!'",
|
||||
[
|
||||
events.disconnect(static_timestamp, '1')
|
||||
]
|
||||
),
|
||||
(
|
||||
"client disconnected 'Client1'(id:1) reason 'invokerid=1"
|
||||
" invokername=Client2 invokeruid=UIDClient2 reasonmsg'",
|
||||
[
|
||||
events.disconnect(static_timestamp, '1'),
|
||||
events.nick(None, 'UIDClient2', 'Client2'),
|
||||
events.kick(None, 'UIDClient2', '1')
|
||||
]
|
||||
),
|
||||
(
|
||||
"client disconnected 'Client1'(id:1) reason 'invokerid=2 "
|
||||
"invokername=Client2 invokeruid=UIDClient2 reasonmsg bantime=0'",
|
||||
[
|
||||
events.disconnect(static_timestamp, '1'),
|
||||
events.nick(None, 'UIDClient2', 'Client2'),
|
||||
events.ban(None, 'UIDClient2', '1')
|
||||
]
|
||||
)
|
||||
])
|
||||
def test_parse_line(line, expected_events):
|
||||
line = '2015-05-18 15:52:52.685612|INFO |VirtualServerBase| 3| ' + line
|
||||
expected_events.insert(0, events.nick(None, '1', 'Client1'))
|
||||
expected_events = [
|
||||
event._replace(timestamp=static_timestamp) for event in expected_events
|
||||
]
|
||||
assert _parse_line(line) == expected_events
|
||||
|
|
|
@ -1,40 +1,36 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import logging
|
||||
from os import remove
|
||||
|
||||
import pendulum
|
||||
import pytest
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
from tsstats.log import parse_log
|
||||
from tsstats.template import render_template
|
||||
from tsstats.utils import seconds_to_text
|
||||
from tsstats.log import parse_logs
|
||||
from tsstats.template import render_servers
|
||||
from tsstats.utils import filter_threshold, seconds_to_text, sort_clients
|
||||
|
||||
output_path = 'tsstats/tests/res/output.html'
|
||||
clients = parse_log('tsstats/tests/res/test.log')
|
||||
servers = list(parse_logs('tsstats/tests/res/test.log', online_dc=False))
|
||||
servers[0] = servers[0]._replace(sid=1) # add missing sid to server object
|
||||
clients = servers[0].clients
|
||||
|
||||
logger = logging.getLogger('tsstats')
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def output(request):
|
||||
def clean():
|
||||
remove('tsstats/tests/res/output.html')
|
||||
request.addfinalizer(clean)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def soup(output):
|
||||
render_template(clients, output_path)
|
||||
return BeautifulSoup(open(output_path), 'html.parser')
|
||||
render_servers(servers, output)
|
||||
return BeautifulSoup(open(output), 'html.parser')
|
||||
|
||||
|
||||
def test_debug(output):
|
||||
logger.setLevel(logging.DEBUG)
|
||||
render_template(clients, output_path)
|
||||
render_servers(servers, output)
|
||||
logger.setLevel(logging.INFO)
|
||||
soup = BeautifulSoup(open(output_path), 'html.parser')
|
||||
soup = BeautifulSoup(open(output), 'html.parser')
|
||||
# check debug-label presence
|
||||
assert soup.find_all(class_='alert alert-danger')
|
||||
for client_item in soup.find_all('li'):
|
||||
assert soup.find('nav').find('div', id='main-nav').find('span').text \
|
||||
== 'debug mode'
|
||||
for client_item in soup.find('ul', id='1.onlinetime').find_all('li'):
|
||||
nick = client_item.find('span').text
|
||||
# check for right identifier
|
||||
nick, encl_identifier = nick.split()
|
||||
|
@ -43,19 +39,43 @@ def test_debug(output):
|
|||
|
||||
|
||||
def test_onlinetime(soup):
|
||||
# move this into a (parameterized) fixture or function
|
||||
items = soup.find('ul', id='onlinetime').find_all('li')
|
||||
nick_data = {}
|
||||
for item in items:
|
||||
nick, data = item.find_all('span')
|
||||
nick_data[nick.text] = data.text
|
||||
# seperate between uuid and id-clients or merge them some way
|
||||
# => assert len(items) == len(clients.id)
|
||||
items = soup.find('ul', id='1.onlinetime').find_all('li')
|
||||
assert len(items) == 2
|
||||
for client in clients:
|
||||
if client.nick in nick_data and client.onlinetime > 0:
|
||||
# remove this clause after splitting cients
|
||||
# (uuid-clients will never have a online-time, because
|
||||
# they're only used for bans and kicks)
|
||||
assert nick_data[client.nick] == \
|
||||
seconds_to_text(client.onlinetime)
|
||||
for item in items:
|
||||
nick, onlinetime = item.find_all('span')
|
||||
nick = nick.text
|
||||
onlinetime = onlinetime.text
|
||||
# find corresponding client-object
|
||||
client = list(filter(
|
||||
lambda c: c.nick == nick and c.onlinetime > pendulum.duration(),
|
||||
clients.values()
|
||||
))
|
||||
# assert existence
|
||||
assert client
|
||||
client = client[0]
|
||||
# compare onlinetimes
|
||||
client_onlinetime_text = seconds_to_text(
|
||||
int(client.onlinetime.total_seconds())
|
||||
)
|
||||
assert onlinetime == client_onlinetime_text
|
||||
|
||||
|
||||
def test_filter_threshold():
|
||||
sorted_clients = sort_clients(
|
||||
clients, lambda c: c.onlinetime.total_seconds())
|
||||
assert len(filter_threshold(sorted_clients, -1)) == len(sorted_clients)
|
||||
assert len(filter_threshold(sorted_clients, 20)) == 1
|
||||
assert len(filter_threshold(sorted_clients, 500)) == 0
|
||||
|
||||
|
||||
def test_lastseen_relative(output):
|
||||
render_servers(servers, output, lastseen_relative=True)
|
||||
soup = BeautifulSoup(open(output), 'html.parser')
|
||||
assert soup.find('ul', id='1.onlinetime')\
|
||||
.select('div.hint--left')[0]['data-hint'] == \
|
||||
pendulum.datetime(2015, 5, 18).diff_for_humans()
|
||||
render_servers(servers, output, lastseen_relative=False)
|
||||
soup = BeautifulSoup(open(output), 'html.parser')
|
||||
assert soup.find('ul', id='1.onlinetime')\
|
||||
.select('div.hint--left')[0]['data-hint'] == \
|
||||
'05/18/15 15:54:38 UTC'
|
||||
|
|
|
@ -1,20 +1,21 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
|
||||
def sort_clients(clients, key):
|
||||
def sort_clients(clients, key_l):
|
||||
'''
|
||||
sort `clients` by `key`
|
||||
|
||||
:param clients: clients to sort
|
||||
:param key: key to sort clients with
|
||||
:param key_l: lambda/function returning the value of `key` for a client
|
||||
|
||||
:type clients: tsstats.client.Clients
|
||||
:type key: str
|
||||
:type key_l: function
|
||||
|
||||
:return: sorted `clients`
|
||||
:rtype: list
|
||||
'''
|
||||
cl_data = [(client, client[key]) for client in clients if client[key] > 0]
|
||||
cl_data = [
|
||||
(client, key_l(client)) for client in clients.values()
|
||||
if key_l(client) > 0
|
||||
]
|
||||
return sorted(cl_data, key=lambda data: data[1], reverse=True)
|
||||
|
||||
|
||||
|
@ -34,3 +35,35 @@ def seconds_to_text(seconds):
|
|||
minutes = str(minutes) + 'm ' if minutes > 0 else ''
|
||||
seconds = str(seconds) + 's' if seconds > 0 else ''
|
||||
return hours + minutes + seconds
|
||||
|
||||
|
||||
def filter_threshold(clients, threshold):
|
||||
'''
|
||||
Filter clients by threshold
|
||||
|
||||
:param clients: List of clients as returned by tsstats.utils.sort_clients
|
||||
:type clients: list
|
||||
|
||||
:return: Clients matching given threshold
|
||||
:rtype: list
|
||||
'''
|
||||
return list(filter(lambda c: c[1] > threshold, clients))
|
||||
|
||||
|
||||
def transform_pretty_identmap(pretty_identmap):
|
||||
'''
|
||||
Transforms a list of client ID mappings from a more descriptive format
|
||||
to the traditional format of alternative IDs to actual ID.
|
||||
|
||||
:param pretty_identmap: ID mapping in "nice" form
|
||||
:type pretty_identmap: list
|
||||
|
||||
:return: ID mapping in simple key/value pairs
|
||||
:rtype: dict
|
||||
'''
|
||||
|
||||
final_identmap = {}
|
||||
for mapping in pretty_identmap:
|
||||
for alt_id in mapping['alternate_ids']:
|
||||
final_identmap[alt_id] = mapping['primary_id']
|
||||
return final_identmap
|
||||
|
|
Loading…
Add table
Reference in a new issue