1
0
Fork 0
mirror of https://github.com/Thor77/TeamspeakStats.git synced 2025-04-13 00:45:33 -04:00

Compare commits

...

337 commits

Author SHA1 Message Date
Thor77
6e069659b7
Bump version to 2.1.0 2023-01-14 19:03:31 +01:00
Thor77
4c872930db
Update pendulum to 2.1.2 2023-01-14 19:02:53 +01:00
Thor77
1a7c1cce9d
Fix secret not passed to publish command 2023-01-14 18:47:58 +01:00
Thor77
dcc26830cb
Remove test call, already running for branch 2023-01-14 18:47:55 +01:00
Thor77
7ae09263e1
Bump version to 2.0.2 2023-01-14 18:41:07 +01:00
Thor77
1d0e3a5fdf Add publish workflow 2023-01-14 18:39:20 +01:00
Thor77
51abb87c88 Fix lint errors 2023-01-14 18:39:20 +01:00
Thor77
43e759c714 Add include and scripts 2023-01-14 18:39:20 +01:00
Thor77
8ff880645a Use poe to run test and lint task 2023-01-14 18:39:20 +01:00
Thor77
4a22a06a91 Drop python 3.6 2023-01-14 18:39:20 +01:00
Thor77
4d09f9d82c Switch to GitHub actions 2023-01-14 18:39:20 +01:00
Thor77
60f102cc34 Switch to Poetry for dependency management 2023-01-14 18:39:20 +01:00
Thor77
8223c389d3 Fix Python 3.10 compatability 2023-01-14 18:39:20 +01:00
Thor77
0a443467be
Remove landscape badge completely 2020-03-27 22:55:51 +01:00
Thor77
561f1b6e4d
Remove landscape badge
because the service seems to be gone
2020-03-27 22:55:10 +01:00
Thor77
e90e3fad43
Drop testing for Python 2 2020-03-27 22:52:34 +01:00
Thor77
61c6022e37
Start testing with Python 3.8 2020-03-27 22:47:12 +01:00
Thor77
f6c5be663a
Update dependencies 2020-03-27 22:47:03 +01:00
Thor77
a9c942d442
Disable shadows on hints 2020-03-27 22:39:50 +01:00
Thor77
127370adbd
Update bootstrap and hint.css 2020-03-27 22:39:37 +01:00
Thor77
b327c4e43d Update dependencies 2019-09-07 15:40:15 +02:00
Thor77
40874d525b
Update dependencies 2019-04-09 18:34:51 +02:00
Thor77
c7f9731b6a
Fix testing requirements filename 2019-03-10 13:16:15 +01:00
Thor77
604dc0a439
Switch to bootstrapcdn for boostrap css 2019-03-10 13:14:17 +01:00
Thor77
b8dbd442f2
Use Python 3.7 for Travis tests 2019-03-10 13:10:37 +01:00
Thor77
18f1576bfb
Remove testing-requirements.txt (replaced by -dev) 2019-03-10 13:10:09 +01:00
Thor77
7f88f0376c
Lock dependencies in requirements.txt 2019-03-10 13:08:57 +01:00
Thor77
1f177ff49b Bump css dependencies 2019-03-10 13:06:28 +01:00
Thor77
544da4062f Use bootstrap-4.0.0 2019-03-10 13:06:28 +01:00
Matthew
35c5c91fb0 Simplify navbar markup, update it for bootstrap4 beta1. ()
* Simplify navbar markup, update it for bootstrap4 beta1.

* Fix CSS classname for background color of navbar
2019-03-10 13:06:28 +01:00
Thor77
21f9f6581b Switch badge-type to secondary
because -default is now invisible
2019-03-10 13:06:28 +01:00
Thor77
371af752eb Add flexbox-classes to listgroup-item
to align badges correctly
2019-03-10 13:06:28 +01:00
Thor77
b81b1bca10 Switch css-include to bootstrap4-beta 2019-03-10 13:06:28 +01:00
Matthew
c34f2d4d8b Improve navbar on mobile devices ()
* add button to show/hide menu items
* ensure navigation fills the width of the viewport by moving it out of the main container element
2019-03-10 13:06:28 +01:00
Thor77
3207c0070c Don't rely on style-attr for debug-label presence 2019-03-10 13:06:28 +01:00
Thor77
4069ab659a Add margin for list-groups 2019-03-10 13:06:28 +01:00
Thor77
0dc12e73a1 Replace deprecated page-header heading-class
with `display-4`
2019-03-10 13:06:28 +01:00
Thor77
b474e4124d Remove now unneccessary padding from body 2019-03-10 13:06:28 +01:00
Thor77
2344915d6e Use new navbar-syntax
removed padding from debug-badge as it's not required anymore
2019-03-10 13:06:28 +01:00
Thor77
3ce4962e12 Use new badge-syntax
explicit definition of badge-style and -position
2019-03-10 13:06:28 +01:00
Thor77
dc572ad8fa Switch css-include to bootstrap4 2019-03-10 13:06:28 +01:00
Thor77
c14e7f1068
Update dependencies 2019-03-10 12:58:36 +01:00
Thor77
687f58e247 Bump version to 2.0.1 2018-05-19 18:10:30 +02:00
Thor77
f878fefeaa Use raw strings for RegEx's 2018-05-19 18:06:59 +02:00
Thor77
187ae0afc5 Lock dependencies in {testing_,}requirements.txt 2018-05-19 18:01:08 +02:00
Thor77
a019278e79 Lock pendulum to 1.5.1 in setup.py 2018-05-19 17:49:43 +02:00
Thor77
e92f5ea5ff Lock pendulum to version 1.5.1
because there are breaking changes in 2.0.0
2018-05-19 17:43:33 +02:00
Thor77
1ef4a3bc15 Handle connected clients on unexpected shutdown
caused, for example, by a server crash.
Could be triggered by an incorrectly named logfile as well because it is
assumed once there are connected clients in a logfile which isn't the
last one to be parsed.

The fix is realized by taking the timestamp of the last event from the
currently parsed logfile and disconnecting all still connected clients
on that timestamp.
2018-05-19 17:28:03 +02:00
Thor77
6e40555612 Only log online_clients if there are any
at the end of a logfile.
2018-05-02 20:59:23 +02:00
Thor77
5d9507deb0 Add Pipfile 2018-03-02 23:59:21 +01:00
Thor77
ae13390b7b Use pylama for stylechecks 2018-02-01 21:49:02 +01:00
Thor77
5ea2f6ab3d Remove extra lines after encoding header 2018-02-01 20:49:23 +01:00
Thor77
489d609807 Add pytests .cache to .gitignore 2018-02-01 17:02:56 +01:00
Thor77
56471137c0 Limit build status on AppVeyor to master-branch 2018-01-30 19:14:51 +01:00
Thor77
4310f93adc Bump version to 2.0.0 2018-01-19 22:28:28 +01:00
Thor77
ab68f57f83 Use timestamp for relative timestamp human test 2018-01-10 20:33:52 +01:00
Thor77
7fd4297c4d Adapt tests to Clients.__iter__ returning keys 2017-09-25 23:21:12 +02:00
Thor77
8d1c19a734 Adapt log.parse_logs and utils.sort_clients
to Clients.__iter__ return keys instead of values
2017-09-25 23:21:06 +02:00
Thor77
2ebd445349 Clients.__iter__ return keys instead of values
as desired by MutableMapping.__iter__
2017-09-25 23:14:26 +02:00
Thor77
c9ab6f6b97 Add encoding-header to missing files 2017-09-16 22:31:15 +02:00
Thor77
c8092018f2 Revert "Sort imports"
because the sorting-order is dependent on installed packages

This reverts commit 51191672c6.
2017-09-16 22:03:55 +02:00
Thor77
51191672c6 Sort imports 2017-09-16 21:59:44 +02:00
Thor77
871210dde4 Add test for tsstats.log._parse_line 2017-09-15 11:35:21 +02:00
Thor77
1e1f112867 Add funcdoc to tsstats.log._parse_line 2017-09-15 10:51:29 +02:00
Thor77
a9e8cd0b6e Revert "Add test for Clients.__str__"
This reverts commit b9f798d04d.
2017-09-14 23:37:10 +02:00
Thor77
8558d731d4 Add test for render_servers.lastseen_relative 2017-09-14 23:09:50 +02:00
Thor77
f786c87dfb Add test for Client.__repr__ 2017-09-14 22:39:12 +02:00
Thor77
52f5cc3ac1 Add test for invalid log line 2017-09-14 22:37:56 +02:00
Thor77
b9f798d04d Add test for Clients.__str__ 2017-09-14 22:37:32 +02:00
Thor77
6345c3f1f5 Remove test for serverstop with connected clients
because it is non-trivial to implement with the new architecture and
probably not worth the effort, because it basically can't happen.
A warning/exception should be added instead.
2017-09-10 00:15:21 +02:00
Thor77
b1b80f657a Use parse_logs instead of _parse_details 2017-09-10 00:04:11 +02:00
Thor77
cbc76b5541 Add missing identmap-lookup for new clients 2017-09-09 18:44:41 +02:00
Thor77
1c224fa0ee Use parse_logs instead of _parse_details 2017-09-09 18:36:43 +02:00
Thor77
c79dd08bc0 Don't accept infinite arguments
leftover from old parse_logs-func
2017-09-09 18:04:56 +02:00
Thor77
147c41ffce Use parse_logs instead of _parse_details
because _parse_details is not available anymore
2017-09-09 17:35:55 +02:00
Thor77
df268f1c2a Add return and rtype to parse_logs-funcdoc 2017-09-08 15:39:39 +02:00
Thor77
602e6c4d51 Log start/end of parsing logfile 2017-09-08 15:39:39 +02:00
Thor77
91a9b8e4c7 Implement online_dc again 2017-09-08 15:39:39 +02:00
Thor77
90a367da27 Warn about online clients on logfile end 2017-09-08 15:39:39 +02:00
Thor77
59d4c88701 Add Client.__repr__ 2017-09-08 15:39:39 +02:00
Thor77
3d6c41538b Indent return of parsed_events correctly 2017-09-08 15:39:39 +02:00
Thor77
96d6e9f050 Only yield Server if there are clients for it 2017-09-08 15:39:39 +02:00
Thor77
7077446627 Add test for Client.nick-property 2017-09-08 15:39:39 +02:00
Thor77
3acf282470 Don't drop current nick in prepare_clients
because that's already handled in the Client.nick-property now
2017-09-08 15:39:39 +02:00
Thor77
c2fb6aa6c1 Handle set_nick event-action with .nick-property 2017-09-08 15:39:39 +02:00
Thor77
088d905196 Convert Client.nick into property
to add previous nick to .nick_history on set
2017-09-08 15:39:39 +02:00
Thor77
a084101ced Add Clients.apply_events
to apply events to a Clients-collection
2017-09-08 15:39:39 +02:00
Thor77
da2b773bf6 Add tsstats/event.py for easy event-initialization 2017-09-08 15:39:39 +02:00
Thor77
08b4e06f10 Refactor and simplify log-parsing
* _parse_line parses one line at a time for simplified testing
and return a list of event.Event's instead of applying changes directly
to a Clients-obj
* parse_log just bundles the logs (using _bundle_logs), opens them,
parses them (using _parse_line) and applies returned events to a
Clients-obj in the correct order

With these changes some sort of caching is possible because events are
not bound to a specific client-object and are easily sortable due to
their attached timestamp.
2017-09-08 15:39:39 +02:00
Thor77
20d40c8890 Add string-representation for Clients
to simplify debug-output
2017-09-08 15:39:39 +02:00
Thor77
caff246f9a Don't raise InvalidLog
because there's nothing you can do about it anyways, so there's no need
to stop.
2017-09-08 14:12:48 +02:00
Thor77
6a84b35a52 [requires.io] Dependency update () 2017-09-05 22:18:04 +02:00
Thor77
7ab4436777 Bump version to 1.5.1 2017-08-24 19:38:44 +02:00
Thor77
3067c229d6 Add pendulum requirement to setup.py 2017-08-24 19:38:20 +02:00
Thor77
7dbd37b028 Add pendulum to testing_requirements.txt 2017-08-03 23:39:50 +02:00
Thor77
22c15202a3 Bump version to 1.5.0 2017-07-07 16:19:28 +02:00
Thor77
edff1e956d Optionally Pendulum.diff_for_humans instead of frmttime
for relative last seen time (default)

* introduce --lastseenabsolute
* introduce tsstats.template.render_servers(lastseen_relative=True)
2017-07-07 14:23:10 +02:00
Thor77
65a8379261 Use pendulum instead of plain datetime
because it is more intuitive to use and doesn't require the
tz_aware_datetime-workaround.
2017-07-07 14:22:24 +02:00
Thor77
3d469ce28c Sort imports 2017-06-02 23:34:33 +02:00
Thor77
6ddc8c94b7 Pass directory to flake8/isort 2017-06-02 23:33:40 +02:00
Thor77
de1cc4be2f Install (testing)requirements for style checks
to fix unexpected results from isort
2017-06-02 23:33:00 +02:00
Thor77
ed62eceda3 Bump version to 1.4.3 2017-06-01 23:41:55 +02:00
Thor77
11acf9f9b6 Add space between identifier and nick
mainly used in debug mode
2017-05-15 22:50:05 +02:00
Thor77
992d35ec87 Fix UnicodeEncodeError in debug mode with Python 2 2017-05-15 22:49:18 +02:00
Thor77
f209573d04 Fix Clients.__add__ not using identmap 2017-05-14 23:28:59 +02:00
Thor77
a4c04e34c8 Close log files after parsing 2017-05-11 11:32:11 +02:00
Thor77
fec833d876 Point to log-directory without glob in example
as the current example will trigger shell-expansion and possibly lead to
confusion (see )
2017-04-30 21:29:49 +02:00
Thor77
8299c73eb9 Move output-fixture to confest.py and yield path
* Modify test_log and test_template to use these changes
2017-04-28 23:27:05 +02:00
Thor77
e37e292ba9 Bump version to 1.4.2 2017-04-19 23:36:30 +02:00
Thor77
cbf7000e91 Include README.rst as description for PyPi 2017-04-19 23:35:25 +02:00
Thor77
ef5894b407 Refactor documentation
* move documentation-source from /docs/source to /docs/ and build in
/docs/_build instead of /docs/build
* convert README to rst and remove information also present in
documentation and refer to it
* regenerate conf.py/Makefile with most recent Sphinx-version
* add development.rst with information for contributors
2017-04-19 23:24:30 +02:00
Thor77
97d58254b3 Remove unnecessary variable-declaration
possibly leading to faster access-time
2017-03-31 22:39:57 +02:00
Thor77
8d18b9c4ae Fix not using defined variable for match-access
possibly leading to slower access-times
2017-03-31 22:21:54 +02:00
Thor77
4b261ed321 Bump version to 1.4.1 2017-03-30 20:15:09 +02:00
Thor77
1bc555d66c Add IdentMap-lookup for new clients
added to clients-collection during log-parsing

Fix 
2017-03-30 20:10:08 +02:00
Thor77
683f9b984a Add testcase for wrong identifier with identmap
If a client connects with a secondary identifier first,
this one is used as the identifier for the Client-object instead of the
primary one.
2017-03-29 12:45:35 +02:00
Thor77
6e662e2555 Bump version to 1.4.0 2017-03-24 23:22:02 +01:00
Thor77
270b20d385 Fix E501 (line too long) 2017-03-24 23:19:48 +01:00
Thor77
0081ac9939 Log parse time of individual logs 2017-03-24 23:18:26 +01:00
Thor77
e4212f28fb Log total execution time to info 2017-03-24 23:17:58 +01:00
Matthew
a83d6de253 Add ability to pass a directory for --log option ()
If you pass a directory, it will now tack a '*.log' glob on the end of
the path to automatically use all log files inside this directory.
2017-03-08 22:54:07 +01:00
Thor77
cb42b9ee2a Bump version 1.3.1 2017-03-04 00:27:38 +01:00
Thor77
bdba91879e Add AppVeyor-badge 2017-03-04 00:25:22 +01:00
Thor77
aaf5f2b851 Convert filter to list
because filters are (non-subscriptable) objects in py3
2017-03-04 00:02:41 +01:00
Thor77
a694a2bc58 Refactor test_onlinetime
to require only one iteration and simplify testing
2017-03-03 23:59:24 +01:00
Thor77
6238f14574 Fix incorrectly initialized codeblock
in IdentMap-documentation
2017-02-28 20:59:09 +01:00
Thor77
cd01eb433a Use hyperlinks instead of onclick-events
to link to positions.
Disabled color-change and text-decoration to keep a clean and consisten
look.
2017-02-27 22:16:16 +01:00
Thor77
a1274a52f4 Bump version to 1.3.0 2017-02-27 13:37:05 +01:00
Thor77
cf82835b46 Exclude tsstats/logger.py from coverage-report 2017-02-27 13:37:05 +01:00
Thor77
c665babc94 Add debugstdout to #Configuration 2017-02-27 13:37:05 +01:00
Thor77
fb5a136b9d Regenerate CLI-usage 2017-02-27 13:37:05 +01:00
Thor77
6c35aed767 Only log to file if not logging to stdout 2017-02-27 13:37:05 +01:00
Thor77
8f49c3e95d Add debugstdout flag/config-directive
to enable debug logging to stdout

Fix 
2017-02-27 13:37:04 +01:00
Thor77
0033ce186f Move logger-setup to tsstats.logger
and handlers are now attached as desired in __main__.cli
2017-02-27 13:37:00 +01:00
Thor77
2753f548fe Test disconnect on server-stop 2017-02-23 21:51:42 +01:00
Thor77
962fd486af Test reading config from disk again 2017-02-23 21:38:59 +01:00
Thor77
709c573b65 Refactor config-tests
by just testing the return-value of tsstats.config.load instead of
writing and reading a config-file from disk as there's absolutely no
need to test that as it's just basic ConfigParser-functionality
2017-02-22 22:00:09 +01:00
Thor77
5e19e38965 Fix E501 (line too long) 2017-02-19 17:17:14 +01:00
Thor77
f68986117b Use flake8 instead of pyflakes
for additional pep8-checks
2017-02-19 17:14:49 +01:00
Thor77
f893c42b31 Bump version to 1.2.0 2017-02-18 15:00:39 +01:00
Matthew
5968dc31dd Support for a nicer structure for ID maps ()
This adds support for a more expressive (albeit more verbose) IdentMap
structure. It makes it easier to annotate the structure with additional
data (such as names to associate with the IDs), to assist with
maintaining the IdentMap.
2017-02-18 14:58:23 +01:00
Thor77
2816c2ecfa Merge pull request from djmattyg007/mobile_improvements
Improve presentation on mobile devices
2017-02-18 14:19:52 +01:00
Thor77
5db1345717 Bump version to 1.1.2 2017-02-16 22:49:27 +01:00
Thor77
4adfb9cfc1 Make all datetime-objects timezone-aware
Because the tool is using utc-timestamps everywhere, this emphasizes
this fact (by default) in the output.
If you don't want timezones behind each datetime in your output, just
remove the "%Z" from the `datetimeformat`.

Fix 
2017-02-16 22:44:51 +01:00
Matthew Gamble
531f5c57d2
Improve presentation at mobile
- Add custom hint--medium--xs style, to make the tooltips wrap neatly
- Add meta viewport tag
2017-02-16 18:17:29 +11:00
Thor77
ba8b393b76 Bump version to 1.1.1 2017-02-15 22:05:20 +01:00
Thor77
9d5547ccfd Run tests for py3.5 on Travis 2017-02-15 21:55:38 +01:00
Thor77
a47c7a6728 Add supported python-versions
Fix 
2017-02-15 21:55:11 +01:00
Thor77
f0c33b2ad9 Remove obsolete TODO-section
there are GitHub-issues for that usecase
2017-02-15 21:53:26 +01:00
Thor77
d5c3f312df Merge pull request from djmattyg007/template_fixes
Fix closing header tag
2017-02-14 22:39:40 +01:00
Thor77
ab96dd42f9 Merge pull request from djmattyg007/readme_update
Update readme to emphasise fact that no ServerQuery account is required
2017-02-14 22:38:26 +01:00
Matthew Gamble
3824eeac10
Fix closing header tag 2017-02-15 08:36:01 +11:00
Matthew Gamble
69557a94a6
Update CLI help text to match readme 2017-02-15 08:33:17 +11:00
Thor77
ae35a73a64 Merge pull request from djmattyg007/template_security
* Add rel=noopener to the github link to prevent window.opener attacks.
* Add no-referrer referrer policy to prevent leakage of sensitive info such as private domain names.
2017-02-14 16:07:37 +01:00
Thor77
d7876f25b6 Merge pull request from djmattyg007/template_doctype
Add doctype to template
2017-02-14 15:58:07 +01:00
Matthew Gamble
96156ca622
Add no-referrer referrer policy to prevent leakage of sensitive info
This prevents Referer headers from being sent when requesting any
external assets, and when clicking on any offsite links. This includes
the github link in the footer. This helps to prevent the leakage of
sensitive details, such as private domain names.
2017-02-14 18:17:35 +11:00
Matthew Gamble
e94e117242
Add rel=noopener to prevent window.opener attacks
This is highly unlikely, but it never hurts to be cautious.
2017-02-14 18:14:59 +11:00
Matthew Gamble
13ad296459
Add doctype to main template 2017-02-14 18:11:35 +11:00
Matthew Gamble
d75779a9f2
Update readme to emphasise fact that no ServerQuery account is required 2017-02-14 18:00:55 +11:00
Thor77
72c1eb78f8 Use fixture for providing clients
for test_client and test_ident_map
2017-02-12 21:38:29 +01:00
Thor77
fe0965db32 Bump version to 1.1.0 2017-02-11 22:24:01 +01:00
Thor77
426c0199b4 Update screenshot.png
to show new position of last-seen hint
2017-02-11 22:21:58 +01:00
Thor77
e005fdeb92 Bump python-version for travis to 3.6 2017-02-11 22:16:12 +01:00
Thor77
cc0f1b30f7 Bump LICENSE-year to 2017 2017-02-11 22:15:51 +01:00
Thor77
22f6402bce Add nick-history functionality
hover a nickname to show previous nicks
2017-02-11 22:14:34 +01:00
Thor77
6366d3ebb1 Show last-seen hint only in Onlinetime-section
currently by filtering by headline (probably not a good long-term-solution)
2017-02-10 23:34:23 +01:00
Thor77
f97f309b85 Move last-seen hint to value
and display it on the left side because last-seen is related to onlinetime and not
to the nick.
2017-02-10 23:33:57 +01:00
Thor77
38d497300c Add various python-related paths to .gitignore 2017-01-26 19:11:34 +01:00
Thor77
365994500a Bump version to 1.0.3 2016-11-24 22:58:22 +01:00
Thor77
8d2e0c1345 Add funcdoc for tsstats.template.prepare_clients 2016-11-24 22:57:56 +01:00
Thor77
7a55829d0b Bump version to 1.0.2 2016-11-23 21:24:19 +01:00
Thor77
cb449558e0 Fix onlinetime_threshold not used 2016-11-23 21:23:20 +01:00
Thor77
42fb5f30b5 Bump version to 1.0.1 2016-11-23 21:19:47 +01:00
Thor77
17a486bb7d Fix incorrectly sorted imports 2016-11-23 21:19:21 +01:00
Thor77
e7e8612bc0 Bump version to 1.0.0 🎉 2016-11-23 21:16:10 +01:00
Thor77
ba47122f50 Sort servers by sid for consistent output 2016-11-23 21:09:11 +01:00
Thor77
ef66b45e21 Prefix clientlist-filter with section-filter
because navbar-entries are also li
2016-11-23 21:06:55 +01:00
Thor77
2cdcdd1e5b Use new, sid-prefixed section-id 2016-11-23 21:06:11 +01:00
Thor77
a96200dafa Update test_debug for new debug-label 2016-11-23 21:04:51 +01:00
Thor77
a3d4cdde5b Use render_servers instead of render_template 2016-11-23 21:02:05 +01:00
Thor77
418be10603 Use render_servers for utf8-write-test 2016-11-22 23:08:49 +01:00
Thor77
42796be9d8 Use render_servers instead of render_template
* limit to one output file for all virtual servers
2016-11-22 23:04:18 +01:00
Thor77
5915664605 Set index.jinja2 as default for General.template 2016-11-22 23:03:51 +01:00
Thor77
062da49244 Rename render_template to render_servers
also includes refactoring for multiserver-output

* now accepts list of servers instead of clients as input
* index.jinja2 is now the default template
2016-11-22 23:02:26 +01:00
Thor77
9a686f7a2f Refactor template(s) for multiserver-display
* all virtual servers are now included in one single outputfile
* navbar for navigation between virtual servers
2016-11-22 22:59:57 +01:00
Thor77
4f77b2b6f3 Bump version to 0.18.0 2016-11-19 22:52:29 +01:00
Thor77
51225175ce Refactor clients preperation into prepare_clients 2016-11-19 22:50:28 +01:00
Thor77
cbbedfa669 Bump version to 0.17.0 2016-11-19 22:29:43 +01:00
Thor77
75c5ebb575 Adapt to new return type of tsstats.log.parse_logs
in this case using list- instead of dict-indexing
2016-11-19 22:28:34 +01:00
Thor77
f0dc95a583 Adapt to new return type of tsstats.log.parse_logs 2016-11-19 22:27:44 +01:00
Thor77
fdbde18856 parse_logs: Return Server instead of dict
because it's easier to iterate and access contained information
2016-11-19 22:25:57 +01:00
Thor77
6003543309 Bump version to 0.16.0 2016-11-18 22:14:00 +01:00
Thor77
117dff2486 Use TemplateStream.dump for writing template
instead of manually opening a file and writing to it
2016-11-18 22:11:14 +01:00
Thor77
87e5d697cb Bump version to 0.15.0 2016-11-18 22:05:29 +01:00
Thor77
9c09f34f02 Update template-location for package_data 2016-11-18 21:55:33 +01:00
Thor77
273a01bec6 Rename default template to stats.jinja2
from template.html
2016-11-18 21:52:30 +01:00
Thor77
9a38e0e7cb Rename template_path to template 2016-11-18 21:46:38 +01:00
Thor77
4bed26dac8 Move default template into new templates-subdir 2016-11-18 21:44:53 +01:00
Thor77
b8d2df4650 Load templates from templates-subdirectory
instead of package-root
2016-11-18 21:42:27 +01:00
Thor77
3acb1af132 Add onlinetimethreshold to Configuration-section 2016-11-14 21:28:02 +01:00
Thor77
5b2ad90436 Bump version to 0.14.1 2016-11-11 18:46:35 +01:00
Thor77
2e234d00bb Update CLI-Usage: add onlinetimethreshold-arg 2016-11-11 18:45:52 +01:00
Thor77
5dc21e89de Bump version to 0.14.0 2016-11-11 18:42:22 +01:00
Thor77
d522ce648a Add testcase for tsstats.utils.filter_threshold 2016-11-11 18:41:51 +01:00
Thor77
5637a11b2b Add onlinetimethreshold config and cli-option
to set render_template.onlinetime_threshold.
Default value is -1 => no filtering to not cause confusion
2016-11-11 18:40:52 +01:00
Thor77
4ac27143a6 Add onlinetime_threshold-arg to render_template
and filter_threshold-function to tsstats.utils.
Only display clients in onlinetime-section with a onlinetime
greater than onlinetime_threshold seconds
2016-11-11 18:40:30 +01:00
Thor77
17392494ed Bump version to 0.13.2 2016-11-10 21:58:30 +01:00
Thor77
414054c243 Link to section-list from section-heading 2016-11-10 21:57:30 +01:00
Thor77
15a437ea2c Bump hint.css-version to 2.4.1 2016-11-10 21:50:45 +01:00
Thor77
338f6cd6de Bump version to 0.13.1 2016-11-07 20:25:24 +01:00
Thor77
1ec0a37480 Add testcase for (encoding issues) 2016-11-07 20:24:08 +01:00
Thor77
db24696c47 Bump version to 0.13.0 2016-11-06 19:28:21 +01:00
Thor77
03c0941962 Add safety-measure to ensure clients disconnect at server stop 2016-11-06 19:23:00 +01:00
Thor77
c8955bee5e Remove double-iteratation over logfiles
by passing the iterator returned by glob.glob direclty to _bundle_logs
2016-11-06 19:18:21 +01:00
Thor77
679473f7b4 Use utcnow for creation_time
because utc timestamps are used in logs and for online_dc, too.
2016-11-04 20:05:02 +01:00
Thor77
812916f179 Use list-comprehension for reconnect
to make this action more obvious and understandable
2016-11-04 20:03:46 +01:00
Thor77
6c301c2ed4 Bump version to 0.12.6 2016-11-03 23:01:09 +01:00
Thor77
ba8e082e64 Reconnect clients only for last log (online_dc)
because doing it after every log would lead to insane onlinetimes
and only the last log should have online clients, anyways.

Fix 
2016-11-03 22:52:05 +01:00
Thor77
edc914b451 Bump version to 0.12.5 2016-11-02 21:24:36 +01:00
Thor77
7cd057d3c5 Write template-output as utf-8 2016-11-02 21:24:19 +01:00
Thor77
c0096a7c52 Read logs as utf-8 2016-11-02 21:24:07 +01:00
Thor77
1c4ee4d72d Import Config-table from README into docs 2016-11-01 22:14:15 +01:00
Thor77
de7afd87e7 Rename CMD-Arguments to CLI-Usage 2016-10-30 21:08:27 +01:00
Thor77
419164f332 Add Example-section to showcase common calls 2016-10-30 21:07:00 +01:00
Thor77
96ffef1b5a Remove ID-Mapping-Section (superseded by docs)
because link in config-section refers to docs anyways
2016-10-30 20:47:35 +01:00
Thor77
8020e047f6 Show more distinct that a config is optional 2016-10-30 20:41:38 +01:00
Thor77
b520fe588f Update cli help output 2016-10-30 19:53:26 +01:00
Thor77
4c89eb8302 Create Contributing-section (superseding "Tests")
and add a short sentence about flake8
2016-10-30 19:49:19 +01:00
Thor77
e5be93b17a Bump version to 0.12.4 2016-10-28 23:43:09 +02:00
Thor77
a1f535961e Add manual installation and package-usage to Installation 2016-10-28 23:42:44 +02:00
Thor77
1a485c67c5 Update screenshot to showcase last-connected-hint 2016-10-28 23:33:14 +02:00
Thor77
de5f58cb93 Bump version to 0.12.3 2016-10-28 23:27:05 +02:00
Thor77
37b8b7bc26 Bump hint.css-version to 2.4.0 2016-10-28 23:24:31 +02:00
Thor77
76948713a4 Bump version to 0.12.2 2016-10-27 00:09:53 +02:00
Thor77
2d80ba6804 Add action-group to re_dis_connect.
Used to not rely on position in message
for correct action-parsing.
2016-10-27 00:08:28 +02:00
Thor77
c0523717a7 Bump version to 0.12.1 2016-10-27 00:08:12 +02:00
Thor77
b6e73e733d Fix adding server/clientgroup-actions to clients.
This was caused, because the used regex was very generic.
Therefore it parsed these lines without any problems,
a timestamp wasn't added but it was already in the clients-dict,
when these checks (conn or disconnect) were performed.
2016-10-27 00:02:56 +02:00
Thor77
80df2c02f0 Add testcase for logs with server/clientgroup-actions 2016-10-27 00:02:17 +02:00
Thor77
9c3c772db9 Bump version to 0.12.0 2016-10-26 23:07:58 +02:00
Thor77
1b1ed86750 Add session-time on disconnect to debug-log 2016-10-04 15:21:05 +02:00
Thor77
040b451c7d Add timestamp for (dis)connect to debug-log 2016-10-04 15:18:08 +02:00
Thor77
c59d182da4 Update screenshot to showcase new features 2016-09-30 22:36:12 +02:00
Thor77
9682e2fe6a Update copyright-year and add copyright-holder 2016-09-30 22:23:07 +02:00
Thor77
45f9ba2737 Bump version to 0.11.2 2016-09-22 15:36:01 +02:00
Thor77
f7669792c1 Set default for General.output to tsstats.html 2016-09-22 15:35:39 +02:00
Thor77
3a1d51a60b Remove default for output-flag
default still set through config-defaults, though
Providing a default here suppressed config-option "output"
2016-09-22 15:28:42 +02:00
Thor77
d1627d369f Add debug-logging-output to tsstats.template.render_template
* bump version to 0.11.1
2016-09-18 21:51:24 +02:00
Thor77
959423be81 Bump version to 0.11.0 2016-09-17 22:51:50 +02:00
Thor77
ddd220d629 Include datetime_fmt in funcdoc of tsstats.template.render_template 2016-09-17 22:49:05 +02:00
Thor77
08d282b6ca Add datetimeformat flag/option to cmd/config section 2016-09-17 22:47:04 +02:00
Thor77
799f622201 Use RawConfigParser instead of ConfigParser in tsstats.config
because General.datetimeformat creates problems with string-interpolation.
These are solved by swichting to RawConfigParser, which doesn't support this feature.
2016-09-17 22:42:32 +02:00
Thor77
52fc1b487c Add cli-flag for datetimeformat and pass config-option to funccall 2016-09-17 22:41:38 +02:00
Thor77
4078e4b06b Add config-option datetimeformat
for tsstats.template.render_template(datetime_fmt)
2016-09-17 22:41:03 +02:00
Thor77
1ecf24b9b2 Add datetime_fmt-arg to tsstats.template.render_template
Specify a custom datetime-format for various datetime-renderings (creation-time, last
online)
2016-09-17 22:38:30 +02:00
Thor77
bd2157c2b8 Update config- and cli-documentation in README.md to include template-option 2016-08-10 23:11:42 +02:00
Thor77
5c52ab1995 Pass template config/cli-option to render_template in tsstats.__main__.main 2016-08-10 22:49:16 +02:00
Thor77
bbd2ff7a46 Add template_path-kwarg to tsstats.template.render_template 2016-08-10 22:47:19 +02:00
Thor77
604fefe286 Add 'template' config-option 2016-08-10 22:45:14 +02:00
Thor77
07b61d86ce Add -t/--template cli-arg 2016-08-10 22:44:48 +02:00
Thor77
3f140b8d6a Don't add sid-suffix to output if only one vserver occured in parsed logs
* bump version to 0.10.5
2016-08-09 20:37:20 +02:00
Thor77
a475caa7c7 Fix not calling str() on value before adding to config in __main__.cli
* bump version to 0.10.4
2016-08-08 22:25:57 +02:00
Thor77
5ff08e0163 Delay debugfile-creation to suppress needless creation if not in debugmode
* bump version to 0.10.3
2016-08-08 22:10:30 +02:00
Thor77
cfb593ba9c use py2-compatible way to check for section-existance in tsstats.config.load
* bump version to 0.10.2
2016-08-06 21:42:54 +02:00
Thor77
87f9bf43fc rewrite config-tests to work with new return-value of tsstats.config.load
* bump version to 0.10.1
2016-08-06 21:39:36 +02:00
Thor77
708f071033 replace tsstats.config.parse_config with tsstats.config.load, which just returns a configparser.ConfigParser-instance for easier extension
tsstats.__main__.main: now accepts only a configparser.ConfigParser-instance and extracts values from it
tsstats.__main__.cli: defaults from argparser are now suppressed and given cli-args override values from config

* bump version to 0.10.0
2016-08-06 21:36:17 +02:00
Thor77
2a1ab472bb update bootstrap and hint.css version
* bump version to 0.9.1
2016-08-03 22:02:56 +02:00
Thor77
4034410e2d bump version to 0.9.0 2016-07-04 21:18:09 +02:00
Thor77
67c08330d8 reduce size of section-headers (h1 -> h2) 2016-07-04 21:17:34 +02:00
Thor77
402040e2dc add title (configurable) as headline to template 2016-07-04 21:16:58 +02:00
Thor77
60bdcb7aab add credit and render-date/time to template-footer 2016-07-04 21:14:29 +02:00
Thor77
477ca7d739 add timezone to output of tsstats.template.render_template.frmttime 2016-07-04 21:11:34 +02:00
Thor77
ea64ee0ecb bump version to 0.8.0 2016-06-25 20:48:18 +02:00
Thor77
6638eaa044 remove done item (multi vservers) from TODO in README.md 2016-06-25 20:46:46 +02:00
Thor77
c25b51dd90 add onlinedc-config-option to table in README.md 2016-06-25 20:45:46 +02:00
Thor77
6c33524850 update help-output in quickstart-docs 2016-06-25 20:43:59 +02:00
Thor77
34e682cf81 remove unused var-assignment in __main__.cli 2016-06-25 20:42:50 +02:00
Thor77
bb808ca8c0 rename noonlinedc to onlinedc in __main__.main to correctly represent it's function
* rename noonlinedc to onlinedc in value-dict returned by .parse_args
* rename keyword-arg from noonlinedc to onlinedc in __main__.main
2016-06-25 20:42:50 +02:00
Thor77
892680fe4d add onlinedc config-option
* inverse of --noonlinedc
2016-06-25 20:42:46 +02:00
Thor77
573838c35e Merge branch 'multi_vserver' 2016-06-24 21:43:51 +02:00
Thor77
487a50508f add -nod/--noonlinedc cli-flag 2016-06-23 21:43:40 +02:00
Thor77
95c22dde35 make __main__.main work with new output of tsstats.log.parse_logs
* if sid given, add .<sid> to outputpath
2016-06-22 20:47:12 +02:00
Thor77
eafc98f548 add test for tsstats.log.parse_logs 2016-06-22 20:40:55 +02:00
Thor77
0b667f55b7 check for None instead of False to allow empty clients as arg to tsstats.log._parse_details 2016-06-22 20:40:30 +02:00
Thor77
def9f2e1e2 fix logpath not given to _bundle_logs in tsstats.log.parse_logs 2016-06-21 22:16:39 +02:00
Thor77
21be54675f add proper func-doc to tsstats.log.parse_logs 2016-06-21 18:29:59 +02:00
Thor77
54532fd598 use _bundle_logs in template-tests for now 2016-06-21 18:25:38 +02:00
Thor77
468bfcd22d fix log-tests by renaming all occurences of parse_log to _parse_details
* additionally remove deprecated test_multiple-test
2016-06-21 18:23:35 +02:00
Thor77
c132c17661 add ident_map-arg to tsstats.log.parse_logs
* give all given arguments to tsstats.log._bundle_logs while parsing (*args,
**kwargs)
2016-06-21 18:20:57 +02:00
Thor77
c0f1a6c649 add glob-resolution to tsstats.log.parse_logs
* correct name of bundle-function (to tsstats.log._bundle_logs)
* add .items() to correct for-loop
2016-06-20 22:02:55 +02:00
Thor77
9a6bbe4f3e fix 2 typos in tsstats.log._bundle_logs 2016-06-20 21:59:46 +02:00
Thor77
976d40e2b9 add test for tsstats.log._bundle_logs 2016-06-20 21:59:22 +02:00
Thor77
105f464b9a sort logs by path, if timestamp not available, though 2016-06-20 21:38:57 +02:00
Thor77
e92ad9e6fe update scope of tsstats.log._sort_logs (rename to tsstats.log._bundle_logs)
* rename to tsstats.log._bundle_logs
* expect list of logpaths as argument and return them sorted
* move globbing-stuff to parse_logs in an upcoming commit
2016-06-20 21:31:59 +02:00
Thor77
0aa0c7b7ea add multi-vserver-support to todo and link to wip 2016-06-19 22:37:16 +02:00
Thor77
612055a088 add wip tsstats.log.parse_logs 2016-06-19 22:26:19 +02:00
Thor77
0c57b27abc update documentation of tsstats.log._sort_logfiles to fit updated scope 2016-06-19 22:21:02 +02:00
Thor77
37a9841900 rename tsstats.log.parse_logs to tsstats.log._sort_logfiles to fit updated scope 2016-06-19 22:13:05 +02:00
Thor77
ad7ff96b1b add filename-checking to split logs by sid
* if filename doesn't match, fallback to plain sorting
* using re_log_filename to match
2016-06-19 22:10:35 +02:00
Thor77
bec0279871 rename tsstats.log.parse_log to tsstats.log._parse_details 2016-06-19 21:44:23 +02:00
Thor77
2821713150 redefine scope of tsstats.log.* in func-docs 2016-06-19 21:37:00 +02:00
Thor77
4c694a7770 bump version to 0.7.2 2016-06-12 18:45:08 +02:00
Thor77
8abb0029fa cover line not matching 2016-06-12 18:44:41 +02:00
Thor77
850d3463ce remove unneccessary and unused tsstats.client.Client.__getitem__ 2016-06-12 18:43:20 +02:00
Thor77
123370554a use defined output_path in tsstats.tests.test_template.output-fixture and sort imports 2016-06-12 18:39:56 +02:00
Thor77
95c516a9f3 make tsstats.tests.test_template.test_online compatible with datetime.timedelta as Client.onlinetime 2016-06-12 18:36:09 +02:00
Thor77
12a86539d2 convert onlinetime to int before converting to text
* => don't display milliseconds in text
2016-06-12 18:11:27 +02:00
Thor77
e3df7f8185 template-filter frmttime just returns formatted timestamp now
* instead of converting it to localtime before
2016-06-12 18:02:47 +02:00
Thor77
2ed3b7f48d make tsstats.tests.test_log compatible with datetime.timedelta as Client.onlinetime 2016-06-12 17:55:52 +02:00
Thor77
32234b4886 rename template-filter fmttime to frmttime 2016-06-12 17:55:03 +02:00
Thor77
c3dabb9082 give .total_seconds() to sort_clients instead of datetime.timedelta 2016-06-12 17:54:32 +02:00
Thor77
28855e9a81 use datetime.timedelta for Client.onlinetime instead of int
* get rid of all the converting-stuff
2016-06-12 17:52:44 +02:00
Thor77
89906d04c7 expect key as lambda in tsstats.utils.sort_clients
* kwarg renamed to key_l (from key)
* add possibility to modify attribute
2016-06-12 17:36:12 +02:00
Thor77
9d5197d813 use py2-compatible way to convert datetime.datetime to timestamp
* bump version to 0.6.8
2016-06-12 17:20:32 +02:00
Thor77
3fec44feaa bump version to 0.6.7 2016-06-12 17:03:09 +02:00
Thor77
276dce0074 give all *args and **kwargs given to tsstats.log.parse_logs directly to parse_log 2016-06-12 17:01:35 +02:00
Thor77
17d7552e8b set parse_log(online_dc=False) in all log-tests 2016-06-12 16:58:17 +02:00
Thor77
4e4eacd3af disconnect online clients after parsing to display correct onlinetime
* toggle by setting tsstats.log(online_dc=)
* fix 
2016-06-12 16:54:49 +02:00
Thor77
a1c5f67c0c extract path to testlog into variable 2016-06-12 16:39:41 +02:00
Thor77
3972594787 add testcase for 2016-06-12 16:38:28 +02:00
Thor77
20cac026d5 bump version to 0.6.6 2016-06-10 16:49:37 +02:00
Thor77
be361e976c sort imports in tsstats/client.py 2016-06-10 16:48:49 +02:00
Thor77
fde3a921f9 refactor tsstats.tests.test_template.test_onlinetime
* check client-count in template
* take clients as base instead for comparisons
* dynamically check all included client-objects
TODO
====
* move nick_data-dict-generation into function or fixture
* split tsstats.client.Clients into id and uuid-clients to easily compare client- and template-data
2016-06-10 16:46:55 +02:00
Thor77
3e5c79fc05 add test for right identifier in debug-mode 2016-06-10 16:22:52 +02:00
Thor77
5d2e3e00a3 add id to sections and use unique id for client-items 2016-06-10 16:04:24 +02:00
Thor77
4ddc6bbf41 refactor tsstats.tests.test_template.test_data
* rename to test_onlinetime
* add soup-fixture (renders template + returns BeautifulSoup-instance)
2016-06-10 15:36:32 +02:00
Thor77
4acfe5bd0b fix references to identmap-doc from tsstats.client and tsstats.log
* use :doc: instead of :ref:
* bump version to 0.6.5
2016-06-08 22:36:26 +02:00
Thor77
f3517dcb99 add quickstart-guide to docs and placeholders for config and cli
* bump version to 0.6.4
2016-06-08 22:36:23 +02:00
Thor77
6074fe0d63 update cli-help and config-keys in README.md
* bump version to 0.6.3
2016-06-08 18:12:57 +02:00
Thor77
ea5d997b5a update bootstrap and hint.css in tsstats/template.html
* bump version to 0.6.2
* fix insecure-content warnings
2016-06-07 18:26:47 +02:00
Thor77
41fee91e63 add tsstats/template.html to setup.py:package_data
* fix TemplateNotFound on pip-installations
* bump version to 0.6.1
2016-06-07 17:59:02 +02:00
Thor77
fe29b6f050 refactor template-loading
* use ChoiceLoader([PackageLoader, FileSystemLoader]) instead of FileSystemLoader
* add PackageLoader to allow template-access on egg-installations
* use correct way to get filesystem-path to package (dirname(__file__))
* remove superflous template_name-arg from tsstats.template.render_template
* bump version to 0.6.0
2016-06-07 17:51:28 +02:00
Thor77
52a05acdab add source-encoding (utf-8) to all files 2016-06-07 17:42:53 +02:00
Thor77
c3cfad0a88 fix wrong regex for logs with unexpected spacing
* add "\ *" arround level, component, sid and message
2016-06-07 16:51:55 +02:00
Thor77
5c44f0ca47 fix missing requirements.txt for installation via pip
* copy deps to setup.py instead of reading requirements.txt
2016-06-07 15:46:10 +02:00
Thor77
8ca23e0115 add documentation to tsstats.{config,log,template,utils}.* 2016-05-30 20:23:03 +02:00
Thor77
b726a9fa79 divite api.rst into sections 2016-05-29 00:26:25 +02:00
Thor77
f7af9f9d39 add documentation for tsstats.exceptions.* 2016-05-29 00:25:21 +02:00
48 changed files with 1928 additions and 640 deletions

View file

@ -8,3 +8,4 @@ exclude_lines =
omit =
tsstats/tests/*
tsstats/__main__.py
tsstats/logger.py

23
.github/workflows/publish.yml vendored Normal file
View 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
View 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
View file

@ -1,2 +1,11 @@
*.pyc
__pycache__/
*.py[cod]
venv/
.cache/
build/
dist/
*.egg-info/
*.egg
docs/_build

View file

@ -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

View file

@ -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.

View file

@ -1,64 +0,0 @@
# TeamspeakStats [![Build Status](https://travis-ci.org/Thor77/TeamspeakStats.svg?branch=master)](https://travis-ci.org/Thor77/TeamspeakStats) [![Coverage Status](https://coveralls.io/repos/Thor77/TeamspeakStats/badge.svg?branch=master&service=github)](https://coveralls.io/github/Thor77/TeamspeakStats?branch=master) [![Code Health](https://landscape.io/github/Thor77/TeamspeakStats/master/landscape.svg?style=flat)](https://landscape.io/github/Thor77/TeamspeakStats/master) [![PyPI](https://img.shields.io/pypi/v/tsstats.svg)](https://pypi.python.org/pypi/tsstats) [![Documentation Status](https://readthedocs.org/projects/teamspeakstats/badge/?version=latest)](http://teamspeakstats.readthedocs.io/en/latest/?badge=latest)
A simple Teamspeak stat-generator - based on server-logs
![screenshot](screenshot.png)
# 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] [--config CONFIG] [--idmap IDMAP] [--debug]
A simple Teamspeak stats-generator - based on server-logs
optional arguments:
-h, --help show this help message and exit
--config CONFIG path to config
--idmap IDMAP path to id_map
--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 |
#### [HTML]
| Key | Description |
|-----|-------------|
| title | HTML-Title of the generated `.html`-file
## 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
View 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

View file

@ -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)

44
docs/api.rst Normal file
View file

@ -0,0 +1,44 @@
API
===
Log
---
.. automodule:: tsstats.log
:members:
Client
------
.. autoclass:: tsstats.client.Client
:members:
.. automethod:: tsstats.client.Client.__init__
.. autoclass:: tsstats.client.Clients
:members:
.. automethod:: tsstats.client.Clients.__init__
.. 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
View 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)

View file

@ -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.5'
version = u'1.4'
# The full version, including alpha/beta/rc tags.
release = '0.5.3'
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
View 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
View 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.

View file

@ -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
View file

@ -0,0 +1,13 @@
TeamspeakStats Documentation
============================
.. toctree::
:maxdepth: 2
cli
config
identmap
api
development
.. include:: ../README.rst

View file

@ -1,13 +0,0 @@
API
***
.. autoclass:: tsstats.client.Client
:members:
.. automethod:: tsstats.client.Client.__init__
.. autoclass:: tsstats.client.Clients
:members:
.. automethod:: tsstats.client.Clients.__init__
.. automethod:: tsstats.client.Clients.__iter__

View file

@ -1,10 +0,0 @@
Welcome to TeamspeakStats's documentation!
==========================================
Contents:
.. toctree::
:maxdepth: 2
api
identmap

512
poetry.lock generated Normal file
View 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
View 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"

View file

@ -1 +0,0 @@
Jinja2>=2.8

Binary file not shown.

Before

(image error) Size: 32 KiB

After

(image error) Size: 33 KiB

View file

@ -1,18 +0,0 @@
from setuptools import setup
setup(
name='tsstats',
version='0.5.3',
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'
]
},
install_requires=open('requirements.txt').read()
)

View file

@ -1,3 +0,0 @@
pytest>=2.9.1
pyflakes>=1.2.2
BeautifulSoup4>=4.4.1

View file

@ -1,13 +1 @@
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)
# -*- coding: utf-8 -*-

View file

@ -1,19 +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',
@ -24,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):
@ -58,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__':

View file

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
import datetime
import logging
from tsstats.exceptions import InvalidLog
from collections import MutableMapping
from collections.abc import MutableMapping
logger = logging.getLogger('tsstats')
@ -14,7 +14,7 @@ class Clients(MutableMapping):
'''
Initialize a new Client-collection
:param ident_map: Identity-map (see :ref:`IdentMap`)
:param ident_map: Identity-map (see :doc:`identmap`)
:type ident_map: dict
'''
self.ident_map = ident_map or {}
@ -22,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
@ -29,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)]
@ -48,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):
@ -66,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`
@ -84,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
@ -95,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
@ -127,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__()

View file

@ -1,26 +1,49 @@
# -*- 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`
:param config_path: path to config-file
:type config_path: str
:return: values of config
: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
View 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
)

View file

@ -1,6 +1,5 @@
# -*- coding: utf-8 -*-
class InvalidConfiguration(Exception):
pass
class InvalidLog(Exception):
pass
'''
The configuration is invalid (either config-file or cli-args)
'''

View file

@ -1,66 +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):
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 _bundle_logs(logs):
'''
Bundle `logs` by virtualserver-id
and sort by timestamp from filename (if exists)
: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_log(log_path, ident_map=None, clients=None):
if not clients:
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)
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('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)
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: list of servers
:rtype: [tsstats.log.Server]
'''
for virtualserver_id, logs in _bundle_logs(glob(log_glob)).items():
clients = Clients(ident_map)
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
View 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)

View file

@ -1,35 +0,0 @@
<html>
<head>
<title>{{ title }}</title>
<meta charset="utf-8">
<link rel="stylesheet" href="http://netdna.bootstrapcdn.com/bootstrap/3.1.1/css/bootstrap.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/hint.css/1.3.5/hint.min.css">
<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 %}
<h1>{{ headline }}</h2>
<ul class="list-group">
{% for client, value in list %}
<li id="{{ client.nick }}" onclick="window.location = '#{{ client.nick }}'" 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>

View file

@ -1,39 +1,110 @@
# -*- coding: utf-8 -*-
import logging
from os.path import abspath
from time import localtime, strftime
from collections import namedtuple
from os.path import dirname, join
from jinja2 import Environment, FileSystemLoader
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, template_name='tsstats/template.html',
title='TeamspeakStats'):
# prepare clients
clients_onlinetime_ = sort_clients(clients, 'onlinetime')
clients_onlinetime = [
(client, seconds_to_text(onlinetime))
for client, onlinetime in clients_onlinetime_
def prepare_clients(clients, onlinetime_threshold=-1):
'''
Prepare `clients` for rendering
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)
)
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)]
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 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
'''
# preparse servers
prepared_servers = [
Server(sid, prepare_clients(clients, onlinetime_threshold))
for sid, clients in servers
]
# render
template_loader = FileSystemLoader(abspath('.'))
template_loader = ChoiceLoader([
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_name)
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)

View 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>

View 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 %}

View file

@ -0,0 +1 @@
# -*- coding: utf-8 -*-

14
tsstats/tests/conftest.py Normal file
View 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

View 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)

View 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!'

View 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

View file

@ -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!'

View file

@ -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

View file

@ -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'

View file

@ -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'

View file

@ -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

View file

@ -1,42 +1,81 @@
# -*- 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)
def soup(output):
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')
# check red label
assert soup.find_all(class_='alert alert-danger')
# check ident present after nick
li = soup.find('li')
assert li
assert '(' in li.text.split()[1]
soup = BeautifulSoup(open(output), 'html.parser')
# check debug-label presence
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()
identifier = encl_identifier.replace('(', '').replace(')', '')
assert clients[identifier].nick == nick
def test_data(output):
render_template(clients, output_path)
soup = BeautifulSoup(open(output_path), 'html.parser')
# check onlinetime-data
assert seconds_to_text(clients['1'].onlinetime) == \
soup.find('span', class_='badge').text
def test_onlinetime(soup):
items = soup.find('ul', id='1.onlinetime').find_all('li')
assert len(items) == 2
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'

View file

@ -1,12 +1,69 @@
def sort_clients(clients, key):
cl_data = [(client, client[key]) for client in clients if client[key] > 0]
# -*- coding: utf-8 -*-
def sort_clients(clients, key_l):
'''
sort `clients` by `key`
:param clients: clients to sort
:param key_l: lambda/function returning the value of `key` for a client
:type clients: tsstats.client.Clients
:type key_l: function
:return: sorted `clients`
:rtype: list
'''
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)
def seconds_to_text(seconds):
'''
convert `seconds` to a text-representation
:param seconds: seconds to convert
:type seconds: int
:return: `seconds` as text-representation
:rtype: str
'''
minutes, seconds = divmod(seconds, 60)
hours, minutes = divmod(minutes, 60)
hours = str(hours) + 'h ' if hours > 0 else ''
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