mirror of
https://github.com/wakatime/sublime-wakatime.git
synced 2023-08-10 21:13:02 +03:00
Compare commits
126 Commits
Author | SHA1 | Date | |
---|---|---|---|
050b14fb53 | |||
c7efc33463 | |||
d0ddbed006 | |||
3ce8f388ab | |||
90731146f9 | |||
e1ab92be6d | |||
8b59e46c64 | |||
006341eb72 | |||
b54e0e13f6 | |||
835c7db864 | |||
53e8bb04e9 | |||
4aa06e3829 | |||
297f65733f | |||
5ba5e6d21b | |||
32eadda81f | |||
c537044801 | |||
a97792c23c | |||
4223f3575f | |||
284cdf3ce4 | |||
27afc41bf4 | |||
1fdda0d64a | |||
c90a4863e9 | |||
94343e5b07 | |||
03acea6e25 | |||
77594700bd | |||
6681409e98 | |||
8f7837269a | |||
a523b3aa4d | |||
6985ce32bb | |||
4be40c7720 | |||
eeb7fd8219 | |||
11fbd2d2a6 | |||
3cecd0de5d | |||
c50100e675 | |||
c1da94bc18 | |||
7f9d6ede9d | |||
192a5c7aa7 | |||
16bbe21be9 | |||
5ebaf12a99 | |||
1834e8978a | |||
22c8ed74bd | |||
12bbb4e561 | |||
c71cb21cc1 | |||
eb11b991f0 | |||
7ea51d09ba | |||
b07b59e0c8 | |||
9d715e95b7 | |||
3edaed53aa | |||
865b0bcee9 | |||
d440fe912c | |||
627455167f | |||
aba89d3948 | |||
18d87118e1 | |||
fd91b9e032 | |||
16b15773bf | |||
f0b518862a | |||
7ee7de70d5 | |||
fb479f8e84 | |||
7d37193f65 | |||
6bd62b95db | |||
abf4a94a59 | |||
9337e3173b | |||
57fa4d4d84 | |||
9b5c59e677 | |||
71ce25a326 | |||
f2f14207f5 | |||
ac2ec0e73c | |||
040a76b93c | |||
dab0621b97 | |||
675f9ecd69 | |||
a6f92b9c74 | |||
bfcc242d7e | |||
762027644f | |||
3c4ceb95fa | |||
d6d8bceca0 | |||
acaad2dc83 | |||
23c5801080 | |||
05a3bfbb53 | |||
8faaa3b0e3 | |||
4bcddf2a98 | |||
b51ae5c2c4 | |||
5cd0061653 | |||
651c84325e | |||
89368529cb | |||
f1f408284b | |||
7053932731 | |||
b6c4956521 | |||
68a2557884 | |||
c7ee7258fb | |||
aaff2503fb | |||
00a1193bd3 | |||
2371daac1b | |||
4395db2b2d | |||
fc8c61fa3f | |||
aa30110343 | |||
b671856341 | |||
b801759cdf | |||
919064200b | |||
911b5656d7 | |||
48bbab33b4 | |||
3b2aafe004 | |||
aa0b2d6d70 | |||
1a6f588d94 | |||
373ebf933f | |||
7fb47228f9 | |||
4fca5e1c06 | |||
cb2d126c47 | |||
17404bf848 | |||
510eea0a8b | |||
d16d1ca747 | |||
440e33b8b7 | |||
307029c37a | |||
60c8ea4454 | |||
e4fe604a93 | |||
308187b2ed | |||
97f4077675 | |||
4960289ed1 | |||
82530cef4f | |||
08172098e2 | |||
56f54fb064 | |||
1bea7cde8c | |||
038847e665 | |||
d233494a39 | |||
070ad5a023 | |||
757a4c6905 | |||
dd61a4f5f4 |
6
Default.sublime-commands
Normal file
6
Default.sublime-commands
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"caption": "WakaTime: Open Dashboard",
|
||||||
|
"command": "wakatime_dashboard"
|
||||||
|
}
|
||||||
|
]
|
329
HISTORY.rst
329
HISTORY.rst
@ -3,6 +3,335 @@ History
|
|||||||
-------
|
-------
|
||||||
|
|
||||||
|
|
||||||
|
6.0.6 (2016-03-06)
|
||||||
|
++++++++++++++++++
|
||||||
|
|
||||||
|
- upgrade wakatime-cli to v4.1.13
|
||||||
|
- encode TimeZone as utf-8 before adding to headers
|
||||||
|
- encode X-Machine-Name as utf-8 before adding to headers
|
||||||
|
|
||||||
|
|
||||||
|
6.0.5 (2016-03-06)
|
||||||
|
++++++++++++++++++
|
||||||
|
|
||||||
|
- upgrade wakatime-cli to v4.1.11
|
||||||
|
- encode machine hostname as Unicode when adding to X-Machine-Name header
|
||||||
|
|
||||||
|
|
||||||
|
6.0.4 (2016-01-15)
|
||||||
|
++++++++++++++++++
|
||||||
|
|
||||||
|
- fix UnicodeDecodeError on ST2 with non-English locale
|
||||||
|
|
||||||
|
|
||||||
|
6.0.3 (2016-01-11)
|
||||||
|
++++++++++++++++++
|
||||||
|
|
||||||
|
- upgrade wakatime-cli core to v4.1.10
|
||||||
|
- accept 201 or 202 response codes as success from api
|
||||||
|
- upgrade requests package to v2.9.1
|
||||||
|
|
||||||
|
|
||||||
|
6.0.2 (2016-01-06)
|
||||||
|
++++++++++++++++++
|
||||||
|
|
||||||
|
- upgrade wakatime-cli core to v4.1.9
|
||||||
|
- improve C# dependency detection
|
||||||
|
- correctly log exception tracebacks
|
||||||
|
- log all unknown exceptions to wakatime.log file
|
||||||
|
- disable urllib3 SSL warning from every request
|
||||||
|
- detect dependencies from golang files
|
||||||
|
- use api.wakatime.com for sending heartbeats
|
||||||
|
|
||||||
|
|
||||||
|
6.0.1 (2016-01-01)
|
||||||
|
++++++++++++++++++
|
||||||
|
|
||||||
|
- use embedded python if system python is broken, or doesn't output a version number
|
||||||
|
- log output from wakatime-cli in ST console when in debug mode
|
||||||
|
|
||||||
|
|
||||||
|
6.0.0 (2015-12-01)
|
||||||
|
++++++++++++++++++
|
||||||
|
|
||||||
|
- use embeddable Python instead of installing on Windows
|
||||||
|
|
||||||
|
|
||||||
|
5.0.1 (2015-10-06)
|
||||||
|
++++++++++++++++++
|
||||||
|
|
||||||
|
- look for python in system PATH again
|
||||||
|
|
||||||
|
|
||||||
|
5.0.0 (2015-10-02)
|
||||||
|
++++++++++++++++++
|
||||||
|
|
||||||
|
- improve logging with levels and log function
|
||||||
|
- switch registry warnings to debug log level
|
||||||
|
|
||||||
|
|
||||||
|
4.0.20 (2015-10-01)
|
||||||
|
++++++++++++++++++
|
||||||
|
|
||||||
|
- correctly find python binary in non-Windows environments
|
||||||
|
|
||||||
|
|
||||||
|
4.0.19 (2015-10-01)
|
||||||
|
++++++++++++++++++
|
||||||
|
|
||||||
|
- handle case where ST builtin python does not have _winreg or winreg module
|
||||||
|
|
||||||
|
|
||||||
|
4.0.18 (2015-10-01)
|
||||||
|
++++++++++++++++++
|
||||||
|
|
||||||
|
- find python location from windows registry
|
||||||
|
|
||||||
|
|
||||||
|
4.0.17 (2015-10-01)
|
||||||
|
++++++++++++++++++
|
||||||
|
|
||||||
|
- download python in non blocking background thread for Windows machines
|
||||||
|
|
||||||
|
|
||||||
|
4.0.16 (2015-09-29)
|
||||||
|
++++++++++++++++++
|
||||||
|
|
||||||
|
- upgrade wakatime cli to v4.1.8
|
||||||
|
- fix bug in guess_language function
|
||||||
|
- improve dependency detection
|
||||||
|
- default request timeout of 30 seconds
|
||||||
|
- new --timeout command line argument to change request timeout in seconds
|
||||||
|
- allow passing command line arguments using sys.argv
|
||||||
|
- fix entry point for pypi distribution
|
||||||
|
- new --entity and --entitytype command line arguments
|
||||||
|
|
||||||
|
|
||||||
|
4.0.15 (2015-08-28)
|
||||||
|
++++++++++++++++++
|
||||||
|
|
||||||
|
- upgrade wakatime cli to v4.1.3
|
||||||
|
- fix local session caching
|
||||||
|
|
||||||
|
|
||||||
|
4.0.14 (2015-08-25)
|
||||||
|
++++++++++++++++++
|
||||||
|
|
||||||
|
- upgrade wakatime cli to v4.1.2
|
||||||
|
- fix bug in offline caching which prevented heartbeats from being cleaned up
|
||||||
|
|
||||||
|
|
||||||
|
4.0.13 (2015-08-25)
|
||||||
|
++++++++++++++++++
|
||||||
|
|
||||||
|
- upgrade wakatime cli to v4.1.1
|
||||||
|
- send hostname in X-Machine-Name header
|
||||||
|
- catch exceptions from pygments.modeline.get_filetype_from_buffer
|
||||||
|
- upgrade requests package to v2.7.0
|
||||||
|
- handle non-ASCII characters in import path on Windows, won't fix for Python2
|
||||||
|
- upgrade argparse to v1.3.0
|
||||||
|
- move language translations to api server
|
||||||
|
- move extension rules to api server
|
||||||
|
- detect correct header file language based on presence of .cpp or .c files named the same as the .h file
|
||||||
|
|
||||||
|
|
||||||
|
4.0.12 (2015-07-31)
|
||||||
|
++++++++++++++++++
|
||||||
|
|
||||||
|
- correctly use urllib in Python3
|
||||||
|
|
||||||
|
|
||||||
|
4.0.11 (2015-07-31)
|
||||||
|
++++++++++++++++++
|
||||||
|
|
||||||
|
- install python if missing on Windows OS
|
||||||
|
|
||||||
|
|
||||||
|
4.0.10 (2015-07-31)
|
||||||
|
++++++++++++++++++
|
||||||
|
|
||||||
|
- downgrade requests library to v2.6.0
|
||||||
|
|
||||||
|
|
||||||
|
4.0.9 (2015-07-29)
|
||||||
|
++++++++++++++++++
|
||||||
|
|
||||||
|
- catch exceptions from pygments.modeline.get_filetype_from_buffer
|
||||||
|
|
||||||
|
|
||||||
|
4.0.8 (2015-06-23)
|
||||||
|
++++++++++++++++++
|
||||||
|
|
||||||
|
- fix offline logging
|
||||||
|
- limit language detection to known file extensions, unless file contents has a vim modeline
|
||||||
|
- upgrade wakatime cli to v4.0.16
|
||||||
|
|
||||||
|
|
||||||
|
4.0.7 (2015-06-21)
|
||||||
|
++++++++++++++++++
|
||||||
|
|
||||||
|
- allow customizing status bar message in sublime-settings file
|
||||||
|
- guess language using multiple methods, then use most accurate guess
|
||||||
|
- use entity and type for new heartbeats api resource schema
|
||||||
|
- correctly log message from py.warnings module
|
||||||
|
- upgrade wakatime cli to v4.0.15
|
||||||
|
|
||||||
|
|
||||||
|
4.0.6 (2015-05-16)
|
||||||
|
++++++++++++++++++
|
||||||
|
|
||||||
|
- fix bug with auto detecting project name
|
||||||
|
- upgrade wakatime cli to v4.0.13
|
||||||
|
|
||||||
|
|
||||||
|
4.0.5 (2015-05-15)
|
||||||
|
++++++++++++++++++
|
||||||
|
|
||||||
|
- correctly display caller and lineno in log file when debug is true
|
||||||
|
- project passed with --project argument will always be used
|
||||||
|
- new --alternate-project argument
|
||||||
|
- upgrade wakatime cli to v4.0.12
|
||||||
|
|
||||||
|
|
||||||
|
4.0.4 (2015-05-12)
|
||||||
|
++++++++++++++++++
|
||||||
|
|
||||||
|
- reuse SSL connection over multiple processes for improved performance
|
||||||
|
- upgrade wakatime cli to v4.0.11
|
||||||
|
|
||||||
|
|
||||||
|
4.0.3 (2015-05-06)
|
||||||
|
++++++++++++++++++
|
||||||
|
|
||||||
|
- send cursorpos to wakatime cli
|
||||||
|
- upgrade wakatime cli to v4.0.10
|
||||||
|
|
||||||
|
|
||||||
|
4.0.2 (2015-05-06)
|
||||||
|
++++++++++++++++++
|
||||||
|
|
||||||
|
- only send heartbeats for the currently active buffer
|
||||||
|
|
||||||
|
|
||||||
|
4.0.1 (2015-05-06)
|
||||||
|
++++++++++++++++++
|
||||||
|
|
||||||
|
- ignore git temporary files
|
||||||
|
- don't send two write heartbeats within 2 seconds of eachother
|
||||||
|
|
||||||
|
|
||||||
|
4.0.0 (2015-04-12)
|
||||||
|
++++++++++++++++++
|
||||||
|
|
||||||
|
- listen for selection modified instead of buffer activated for better performance
|
||||||
|
|
||||||
|
|
||||||
|
3.0.19 (2015-04-07)
|
||||||
|
+++++++++++++++++++
|
||||||
|
|
||||||
|
- fix bug in project detection when folder not found
|
||||||
|
|
||||||
|
|
||||||
|
3.0.18 (2015-04-04)
|
||||||
|
+++++++++++++++++++
|
||||||
|
|
||||||
|
- upgrade wakatime cli to v4.0.8
|
||||||
|
- added api_url config option to .wakatime.cfg file
|
||||||
|
|
||||||
|
|
||||||
|
3.0.17 (2015-04-02)
|
||||||
|
+++++++++++++++++++
|
||||||
|
|
||||||
|
- use open folder as current project when not using revision control
|
||||||
|
|
||||||
|
|
||||||
|
3.0.16 (2015-04-02)
|
||||||
|
+++++++++++++++++++
|
||||||
|
|
||||||
|
- copy list when obfuscating api key so original command is not modified
|
||||||
|
|
||||||
|
|
||||||
|
3.0.15 (2015-04-01)
|
||||||
|
+++++++++++++++++++
|
||||||
|
|
||||||
|
- obfuscate api key when logging to Sublime Text Console in debug mode
|
||||||
|
|
||||||
|
|
||||||
|
3.0.14 (2015-03-31)
|
||||||
|
+++++++++++++++++++
|
||||||
|
|
||||||
|
- always use external python binary because ST builtin python does not support checking SSL certs
|
||||||
|
- upgrade wakatime cli to v4.0.6
|
||||||
|
|
||||||
|
|
||||||
|
3.0.13 (2015-03-23)
|
||||||
|
+++++++++++++++++++
|
||||||
|
|
||||||
|
- correctly check for SSL support in ST built-in python
|
||||||
|
- fix status bar message
|
||||||
|
|
||||||
|
|
||||||
|
3.0.12 (2015-03-23)
|
||||||
|
+++++++++++++++++++
|
||||||
|
|
||||||
|
- always use unicode function from compat module when encoding log messages
|
||||||
|
|
||||||
|
|
||||||
|
3.0.11 (2015-03-23)
|
||||||
|
+++++++++++++++++++
|
||||||
|
|
||||||
|
- upgrade simplejson package to v3.6.5
|
||||||
|
|
||||||
|
|
||||||
|
3.0.10 (2015-03-22)
|
||||||
|
+++++++++++++++++++
|
||||||
|
|
||||||
|
- ability to disable status bar message from WakaTime.sublime-settings file
|
||||||
|
|
||||||
|
|
||||||
|
3.0.9 (2015-03-20)
|
||||||
|
++++++++++++++++++
|
||||||
|
|
||||||
|
- status bar message showing when WakaTime plugin is enabled
|
||||||
|
- moved some logic into thread to help prevent slow plugin warning message
|
||||||
|
|
||||||
|
|
||||||
|
3.0.8 (2015-03-09)
|
||||||
|
++++++++++++++++++
|
||||||
|
|
||||||
|
- upgrade wakatime cli to v4.0.4
|
||||||
|
- use requests library instead of urllib2, so api SSL cert is verified
|
||||||
|
- new --notfile argument to support logging time without a real file
|
||||||
|
- new --proxy argument for https proxy support
|
||||||
|
- new options for excluding and including directories
|
||||||
|
|
||||||
|
|
||||||
|
3.0.7 (2015-02-05)
|
||||||
|
++++++++++++++++++
|
||||||
|
|
||||||
|
- handle errors encountered when looking for .sublime-project file
|
||||||
|
|
||||||
|
|
||||||
|
3.0.6 (2015-01-13)
|
||||||
|
++++++++++++++++++
|
||||||
|
|
||||||
|
- upgrade external wakatime package to v3.0.5
|
||||||
|
- ignore errors from malformed markup (too many closing tags)
|
||||||
|
|
||||||
|
|
||||||
|
3.0.5 (2015-01-06)
|
||||||
|
++++++++++++++++++
|
||||||
|
|
||||||
|
- upgrade external wakatime package to v3.0.4
|
||||||
|
- remove unused dependency, which is missing in some python environments
|
||||||
|
|
||||||
|
|
||||||
|
3.0.4 (2014-12-26)
|
||||||
|
++++++++++++++++++
|
||||||
|
|
||||||
|
- fix bug causing plugin to not work in Sublime Text 2
|
||||||
|
|
||||||
|
|
||||||
3.0.3 (2014-12-25)
|
3.0.3 (2014-12-25)
|
||||||
++++++++++++++++++
|
++++++++++++++++++
|
||||||
|
|
||||||
|
37
README.md
37
README.md
@ -1,16 +1,15 @@
|
|||||||
sublime-wakatime
|
sublime-wakatime
|
||||||
================
|
================
|
||||||
|
|
||||||
Fully automatic time tracking for Sublime Text 2 & 3.
|
Metrics, insights, and time tracking automatically generated from your programming activity.
|
||||||
|
|
||||||
|
|
||||||
Installation
|
Installation
|
||||||
------------
|
------------
|
||||||
|
|
||||||
Heads Up! For Sublime Text 2 on Windows & Linux, WakaTime depends on [Python](http://www.python.org/getit/) being installed to work correctly.
|
1. Install [Package Control](https://packagecontrol.io/installation).
|
||||||
|
|
||||||
1. Install [Sublime Package Control](https://sublime.wbond.net/installation).
|
|
||||||
|
|
||||||
2. Using [Sublime Package Control](http://wbond.net/sublime_packages/package_control):
|
2. Using [Package Control](https://packagecontrol.io/docs/usage):
|
||||||
|
|
||||||
a) Inside Sublime, press `ctrl+shift+p`(Windows, Linux) or `cmd+shift+p`(OS X).
|
a) Inside Sublime, press `ctrl+shift+p`(Windows, Linux) or `cmd+shift+p`(OS X).
|
||||||
|
|
||||||
@ -18,14 +17,40 @@ Heads Up! For Sublime Text 2 on Windows & Linux, WakaTime depends on [Python](ht
|
|||||||
|
|
||||||
c) Type `wakatime`, then press `enter` with the `WakaTime` plugin selected.
|
c) Type `wakatime`, then press `enter` with the `WakaTime` plugin selected.
|
||||||
|
|
||||||
3. Enter your [api key](https://wakatime.com/settings#apikey) from https://wakatime.com/settings#apikey, then press `enter`.
|
3. Enter your [api key](https://wakatime.com/settings#apikey), then press `enter`.
|
||||||
|
|
||||||
4. Use Sublime and your time will be tracked for you automatically.
|
4. Use Sublime and your time will be tracked for you automatically.
|
||||||
|
|
||||||
5. Visit https://wakatime.com/dashboard to see your logged time.
|
5. Visit https://wakatime.com/dashboard to see your logged time.
|
||||||
|
|
||||||
|
|
||||||
Screen Shots
|
Screen Shots
|
||||||
------------
|
------------
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
|
||||||
|
Unresponsive Plugin Warning
|
||||||
|
---------------------------
|
||||||
|
|
||||||
|
In Sublime Text 2, if you get a warning message:
|
||||||
|
|
||||||
|
A plugin (WakaTime) may be making Sublime Text unresponsive by taking too long (0.017332s) in its on_modified callback.
|
||||||
|
|
||||||
|
To fix this, go to `Preferences > Settings - User` then add the following setting:
|
||||||
|
|
||||||
|
`"detect_slow_plugins": false`
|
||||||
|
|
||||||
|
|
||||||
|
Troubleshooting
|
||||||
|
---------------
|
||||||
|
|
||||||
|
First, turn on debug mode in your `WakaTime.sublime-settings` file.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Add the line: `"debug": true`
|
||||||
|
|
||||||
|
Then, open your Sublime Console with `View -> Show Console` to see the plugin executing the wakatime cli process when sending a heartbeat. Also, tail your `$HOME/.wakatime.log` file to debug wakatime cli problems.
|
||||||
|
|
||||||
|
For more general troubleshooting information, see [wakatime/wakatime#troubleshooting](https://github.com/wakatime/wakatime#troubleshooting).
|
||||||
|
498
WakaTime.py
498
WakaTime.py
@ -6,52 +6,120 @@ License: BSD, see LICENSE for more details.
|
|||||||
Website: https://wakatime.com/
|
Website: https://wakatime.com/
|
||||||
==========================================================="""
|
==========================================================="""
|
||||||
|
|
||||||
__version__ = '3.0.3'
|
|
||||||
|
__version__ = '6.0.6'
|
||||||
|
|
||||||
|
|
||||||
import sublime
|
import sublime
|
||||||
import sublime_plugin
|
import sublime_plugin
|
||||||
|
|
||||||
import glob
|
|
||||||
import os
|
import os
|
||||||
import platform
|
import platform
|
||||||
|
import re
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
import threading
|
import threading
|
||||||
import uuid
|
import urllib
|
||||||
from os.path import expanduser, dirname, basename, realpath, isfile, join, exists
|
import webbrowser
|
||||||
|
from datetime import datetime
|
||||||
|
from zipfile import ZipFile
|
||||||
|
from subprocess import Popen, STDOUT, PIPE
|
||||||
|
try:
|
||||||
|
import _winreg as winreg # py2
|
||||||
|
except ImportError:
|
||||||
|
try:
|
||||||
|
import winreg # py3
|
||||||
|
except ImportError:
|
||||||
|
winreg = None
|
||||||
|
|
||||||
|
|
||||||
|
is_py2 = (sys.version_info[0] == 2)
|
||||||
|
is_py3 = (sys.version_info[0] == 3)
|
||||||
|
|
||||||
|
if is_py2:
|
||||||
|
def u(text):
|
||||||
|
if text is None:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
text = str(text)
|
||||||
|
return text.decode('utf-8')
|
||||||
|
except:
|
||||||
|
try:
|
||||||
|
return text.decode(sys.getdefaultencoding())
|
||||||
|
except:
|
||||||
|
try:
|
||||||
|
return unicode(text)
|
||||||
|
except:
|
||||||
|
return text
|
||||||
|
|
||||||
|
elif is_py3:
|
||||||
|
def u(text):
|
||||||
|
if text is None:
|
||||||
|
return None
|
||||||
|
if isinstance(text, bytes):
|
||||||
|
try:
|
||||||
|
return text.decode('utf-8')
|
||||||
|
except:
|
||||||
|
try:
|
||||||
|
return text.decode(sys.getdefaultencoding())
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
return str(text)
|
||||||
|
except:
|
||||||
|
return text
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise Exception('Unsupported Python version: {0}.{1}.{2}'.format(
|
||||||
|
sys.version_info[0],
|
||||||
|
sys.version_info[1],
|
||||||
|
sys.version_info[2],
|
||||||
|
))
|
||||||
|
|
||||||
|
|
||||||
# globals
|
# globals
|
||||||
ACTION_FREQUENCY = 2
|
HEARTBEAT_FREQUENCY = 2
|
||||||
ST_VERSION = int(sublime.version())
|
ST_VERSION = int(sublime.version())
|
||||||
PLUGIN_DIR = dirname(realpath(__file__))
|
PLUGIN_DIR = os.path.dirname(os.path.realpath(__file__))
|
||||||
API_CLIENT = '%s/packages/wakatime/wakatime-cli.py' % PLUGIN_DIR
|
API_CLIENT = os.path.join(PLUGIN_DIR, 'packages', 'wakatime', 'cli.py')
|
||||||
SETTINGS_FILE = 'WakaTime.sublime-settings'
|
SETTINGS_FILE = 'WakaTime.sublime-settings'
|
||||||
SETTINGS = {}
|
SETTINGS = {}
|
||||||
LAST_ACTION = {
|
LAST_HEARTBEAT = {
|
||||||
'time': 0,
|
'time': 0,
|
||||||
'file': None,
|
'file': None,
|
||||||
'is_write': False,
|
'is_write': False,
|
||||||
}
|
}
|
||||||
HAS_SSL = False
|
|
||||||
LOCK = threading.RLock()
|
LOCK = threading.RLock()
|
||||||
|
PYTHON_LOCATION = None
|
||||||
|
|
||||||
|
|
||||||
|
# Log Levels
|
||||||
|
DEBUG = 'DEBUG'
|
||||||
|
INFO = 'INFO'
|
||||||
|
WARNING = 'WARNING'
|
||||||
|
ERROR = 'ERROR'
|
||||||
|
|
||||||
|
|
||||||
# add wakatime package to path
|
# add wakatime package to path
|
||||||
sys.path.insert(0, join(PLUGIN_DIR, 'packages', 'wakatime'))
|
sys.path.insert(0, os.path.join(PLUGIN_DIR, 'packages'))
|
||||||
|
|
||||||
from wakatime import parseConfigFile
|
|
||||||
|
|
||||||
# check if we have SSL support
|
|
||||||
try:
|
try:
|
||||||
import ssl
|
from wakatime.base import parseConfigFile
|
||||||
import socket
|
except ImportError:
|
||||||
socket.ssl
|
pass
|
||||||
HAS_SSL = True
|
|
||||||
except (ImportError, AttributeError):
|
|
||||||
from subprocess import Popen
|
|
||||||
|
|
||||||
if HAS_SSL:
|
|
||||||
# import wakatime package so we can use built-in python
|
def log(lvl, message, *args, **kwargs):
|
||||||
import wakatime
|
try:
|
||||||
|
if lvl == DEBUG and not SETTINGS.get('debug'):
|
||||||
|
return
|
||||||
|
msg = message
|
||||||
|
if len(args) > 0:
|
||||||
|
msg = message.format(*args)
|
||||||
|
elif len(kwargs) > 0:
|
||||||
|
msg = message.format(**kwargs)
|
||||||
|
print('[WakaTime] [{lvl}] {msg}'.format(lvl=lvl, msg=msg))
|
||||||
|
except RuntimeError:
|
||||||
|
sublime.set_timeout(lambda: log(lvl, message, *args, **kwargs), 0)
|
||||||
|
|
||||||
|
|
||||||
def createConfigFile():
|
def createConfigFile():
|
||||||
@ -60,11 +128,11 @@ def createConfigFile():
|
|||||||
"""
|
"""
|
||||||
configFile = os.path.join(os.path.expanduser('~'), '.wakatime.cfg')
|
configFile = os.path.join(os.path.expanduser('~'), '.wakatime.cfg')
|
||||||
try:
|
try:
|
||||||
with open(configFile, 'r', encoding='utf-8') as fh:
|
with open(configFile) as fh:
|
||||||
pass
|
pass
|
||||||
except IOError:
|
except IOError:
|
||||||
try:
|
try:
|
||||||
with open(configFile, 'w', encoding='utf-8') as fh:
|
with open(configFile, 'w') as fh:
|
||||||
fh.write("[settings]\n")
|
fh.write("[settings]\n")
|
||||||
fh.write("debug = false\n")
|
fh.write("debug = false\n")
|
||||||
fh.write("hidefilenames = false\n")
|
fh.write("hidefilenames = false\n")
|
||||||
@ -78,10 +146,13 @@ def prompt_api_key():
|
|||||||
createConfigFile()
|
createConfigFile()
|
||||||
|
|
||||||
default_key = ''
|
default_key = ''
|
||||||
configs = parseConfigFile()
|
try:
|
||||||
if configs is not None:
|
configs = parseConfigFile()
|
||||||
if configs.has_option('settings', 'api_key'):
|
if configs is not None:
|
||||||
default_key = configs.get('settings', 'api_key')
|
if configs.has_option('settings', 'api_key'):
|
||||||
|
default_key = configs.get('settings', 'api_key')
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
if SETTINGS.get('api_key'):
|
if SETTINGS.get('api_key'):
|
||||||
return True
|
return True
|
||||||
@ -95,58 +166,209 @@ def prompt_api_key():
|
|||||||
window.show_input_panel('[WakaTime] Enter your wakatime.com api key:', default_key, got_key, None, None)
|
window.show_input_panel('[WakaTime] Enter your wakatime.com api key:', default_key, got_key, None, None)
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
print('[WakaTime] Error: Could not prompt for api key because no window found.')
|
log(ERROR, 'Could not prompt for api key because no window found.')
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def python_binary():
|
def python_binary():
|
||||||
if platform.system() == 'Windows':
|
if PYTHON_LOCATION is not None:
|
||||||
try:
|
return PYTHON_LOCATION
|
||||||
Popen(['pythonw', '--version'])
|
|
||||||
return 'pythonw'
|
# look for python in PATH and common install locations
|
||||||
except:
|
paths = [
|
||||||
for path in glob.iglob('/python*'):
|
os.path.join(os.path.expanduser('~'), '.wakatime', 'python'),
|
||||||
if exists(realpath(join(path, 'pythonw.exe'))):
|
None,
|
||||||
return realpath(join(path, 'pythonw'))
|
'/',
|
||||||
return None
|
'/usr/local/bin/',
|
||||||
return 'python'
|
'/usr/bin/',
|
||||||
|
]
|
||||||
|
for path in paths:
|
||||||
|
path = find_python_in_folder(path)
|
||||||
|
if path is not None:
|
||||||
|
set_python_binary_location(path)
|
||||||
|
return path
|
||||||
|
|
||||||
|
# look for python in windows registry
|
||||||
|
path = find_python_from_registry(r'SOFTWARE\Python\PythonCore')
|
||||||
|
if path is not None:
|
||||||
|
set_python_binary_location(path)
|
||||||
|
return path
|
||||||
|
path = find_python_from_registry(r'SOFTWARE\Wow6432Node\Python\PythonCore')
|
||||||
|
if path is not None:
|
||||||
|
set_python_binary_location(path)
|
||||||
|
return path
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def enough_time_passed(now, last_time):
|
def set_python_binary_location(path):
|
||||||
if now - last_time > ACTION_FREQUENCY * 60:
|
global PYTHON_LOCATION
|
||||||
|
PYTHON_LOCATION = path
|
||||||
|
log(DEBUG, 'Found Python at: {0}'.format(path))
|
||||||
|
|
||||||
|
|
||||||
|
def find_python_from_registry(location, reg=None):
|
||||||
|
if platform.system() != 'Windows' or winreg is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if reg is None:
|
||||||
|
path = find_python_from_registry(location, reg=winreg.HKEY_CURRENT_USER)
|
||||||
|
if path is None:
|
||||||
|
path = find_python_from_registry(location, reg=winreg.HKEY_LOCAL_MACHINE)
|
||||||
|
return path
|
||||||
|
|
||||||
|
val = None
|
||||||
|
sub_key = 'InstallPath'
|
||||||
|
compiled = re.compile(r'^\d+\.\d+$')
|
||||||
|
|
||||||
|
try:
|
||||||
|
with winreg.OpenKey(reg, location) as handle:
|
||||||
|
versions = []
|
||||||
|
try:
|
||||||
|
for index in range(1024):
|
||||||
|
version = winreg.EnumKey(handle, index)
|
||||||
|
try:
|
||||||
|
if compiled.search(version):
|
||||||
|
versions.append(version)
|
||||||
|
except re.error:
|
||||||
|
pass
|
||||||
|
except EnvironmentError:
|
||||||
|
pass
|
||||||
|
versions.sort(reverse=True)
|
||||||
|
for version in versions:
|
||||||
|
try:
|
||||||
|
path = winreg.QueryValue(handle, version + '\\' + sub_key)
|
||||||
|
if path is not None:
|
||||||
|
path = find_python_in_folder(path)
|
||||||
|
if path is not None:
|
||||||
|
log(DEBUG, 'Found python from {reg}\\{key}\\{version}\\{sub_key}.'.format(
|
||||||
|
reg=reg,
|
||||||
|
key=location,
|
||||||
|
version=version,
|
||||||
|
sub_key=sub_key,
|
||||||
|
))
|
||||||
|
return path
|
||||||
|
except WindowsError:
|
||||||
|
log(DEBUG, 'Could not read registry value "{reg}\\{key}\\{version}\\{sub_key}".'.format(
|
||||||
|
reg=reg,
|
||||||
|
key=location,
|
||||||
|
version=version,
|
||||||
|
sub_key=sub_key,
|
||||||
|
))
|
||||||
|
except WindowsError:
|
||||||
|
if SETTINGS.get('debug'):
|
||||||
|
log(DEBUG, 'Could not read registry value "{reg}\\{key}".'.format(
|
||||||
|
reg=reg,
|
||||||
|
key=location,
|
||||||
|
))
|
||||||
|
|
||||||
|
return val
|
||||||
|
|
||||||
|
|
||||||
|
def find_python_in_folder(folder, headless=True):
|
||||||
|
pattern = re.compile(r'\d+\.\d+')
|
||||||
|
|
||||||
|
path = 'python'
|
||||||
|
if folder is not None:
|
||||||
|
path = os.path.realpath(os.path.join(folder, 'python'))
|
||||||
|
if headless:
|
||||||
|
path = u(path) + u('w')
|
||||||
|
log(DEBUG, u('Looking for Python at: {0}').format(path))
|
||||||
|
try:
|
||||||
|
process = Popen([path, '--version'], stdout=PIPE, stderr=STDOUT)
|
||||||
|
output, err = process.communicate()
|
||||||
|
output = u(output).strip()
|
||||||
|
retcode = process.poll()
|
||||||
|
log(DEBUG, u('Python Version Output: {0}').format(output))
|
||||||
|
if not retcode and pattern.search(output):
|
||||||
|
return path
|
||||||
|
except:
|
||||||
|
log(DEBUG, u('Python Version Output: {0}').format(u(sys.exc_info()[1])))
|
||||||
|
|
||||||
|
if headless:
|
||||||
|
path = find_python_in_folder(folder, headless=False)
|
||||||
|
if path is not None:
|
||||||
|
return path
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def obfuscate_apikey(command_list):
|
||||||
|
cmd = list(command_list)
|
||||||
|
apikey_index = None
|
||||||
|
for num in range(len(cmd)):
|
||||||
|
if cmd[num] == '--key':
|
||||||
|
apikey_index = num + 1
|
||||||
|
break
|
||||||
|
if apikey_index is not None and apikey_index < len(cmd):
|
||||||
|
cmd[apikey_index] = 'XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXX' + cmd[apikey_index][-4:]
|
||||||
|
return cmd
|
||||||
|
|
||||||
|
|
||||||
|
def enough_time_passed(now, last_heartbeat, is_write):
|
||||||
|
if now - last_heartbeat['time'] > HEARTBEAT_FREQUENCY * 60:
|
||||||
|
return True
|
||||||
|
if is_write and now - last_heartbeat['time'] > 2:
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def find_project_name_from_folders(folders):
|
def find_folder_containing_file(folders, current_file):
|
||||||
for folder in folders:
|
"""Returns absolute path to folder containing the file.
|
||||||
for file_name in os.listdir(folder):
|
"""
|
||||||
if file_name.endswith('.sublime-project'):
|
|
||||||
return file_name.replace('.sublime-project', '', 1)
|
parent_folder = None
|
||||||
return None
|
|
||||||
|
current_folder = current_file
|
||||||
|
while True:
|
||||||
|
for folder in folders:
|
||||||
|
if os.path.realpath(os.path.dirname(current_folder)) == os.path.realpath(folder):
|
||||||
|
parent_folder = folder
|
||||||
|
break
|
||||||
|
if parent_folder is not None:
|
||||||
|
break
|
||||||
|
if not current_folder or os.path.dirname(current_folder) == current_folder:
|
||||||
|
break
|
||||||
|
current_folder = os.path.dirname(current_folder)
|
||||||
|
|
||||||
|
return parent_folder
|
||||||
|
|
||||||
|
|
||||||
def handle_action(view, is_write=False):
|
def find_project_from_folders(folders, current_file):
|
||||||
global LOCK, LAST_ACTION
|
"""Find project name from open folders.
|
||||||
with LOCK:
|
"""
|
||||||
|
|
||||||
|
folder = find_folder_containing_file(folders, current_file)
|
||||||
|
return os.path.basename(folder) if folder else None
|
||||||
|
|
||||||
|
|
||||||
|
def is_view_active(view):
|
||||||
|
if view:
|
||||||
|
active_window = sublime.active_window()
|
||||||
|
if active_window:
|
||||||
|
active_view = active_window.active_view()
|
||||||
|
if active_view:
|
||||||
|
return active_view.buffer_id() == view.buffer_id()
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def handle_heartbeat(view, is_write=False):
|
||||||
|
window = view.window()
|
||||||
|
if window is not None:
|
||||||
target_file = view.file_name()
|
target_file = view.file_name()
|
||||||
if target_file:
|
project = window.project_data() if hasattr(window, 'project_data') else None
|
||||||
project = view.window().project_file_name() if hasattr(view.window(), 'project_file_name') else None
|
folders = window.folders()
|
||||||
if project:
|
thread = SendHeartbeatThread(target_file, view, is_write=is_write, project=project, folders=folders)
|
||||||
project = basename(project).replace('.sublime-project', '', 1)
|
thread.start()
|
||||||
thread = SendActionThread(target_file, is_write=is_write, project=project, folders=view.window().folders())
|
|
||||||
thread.start()
|
|
||||||
LAST_ACTION = {
|
|
||||||
'file': target_file,
|
|
||||||
'time': time.time(),
|
|
||||||
'is_write': is_write,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class SendActionThread(threading.Thread):
|
class SendHeartbeatThread(threading.Thread):
|
||||||
|
"""Non-blocking thread for sending heartbeats to api.
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(self, target_file, is_write=False, project=None, folders=None, force=False):
|
def __init__(self, target_file, view, is_write=False, project=None, folders=None, force=False):
|
||||||
threading.Thread.__init__(self)
|
threading.Thread.__init__(self)
|
||||||
|
self.lock = LOCK
|
||||||
self.target_file = target_file
|
self.target_file = target_file
|
||||||
self.is_write = is_write
|
self.is_write = is_write
|
||||||
self.project = project
|
self.project = project
|
||||||
@ -155,17 +377,20 @@ class SendActionThread(threading.Thread):
|
|||||||
self.debug = SETTINGS.get('debug')
|
self.debug = SETTINGS.get('debug')
|
||||||
self.api_key = SETTINGS.get('api_key', '')
|
self.api_key = SETTINGS.get('api_key', '')
|
||||||
self.ignore = SETTINGS.get('ignore', [])
|
self.ignore = SETTINGS.get('ignore', [])
|
||||||
self.last_action = LAST_ACTION
|
self.last_heartbeat = LAST_HEARTBEAT.copy()
|
||||||
|
self.cursorpos = view.sel()[0].begin() if view.sel() else None
|
||||||
|
self.view = view
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
if self.target_file:
|
with self.lock:
|
||||||
self.timestamp = time.time()
|
if self.target_file:
|
||||||
if self.force or (self.is_write and not self.last_action['is_write']) or self.target_file != self.last_action['file'] or enough_time_passed(self.timestamp, self.last_action['time']):
|
self.timestamp = time.time()
|
||||||
self.send()
|
if self.force or self.target_file != self.last_heartbeat['file'] or enough_time_passed(self.timestamp, self.last_heartbeat, self.is_write):
|
||||||
|
self.send_heartbeat()
|
||||||
|
|
||||||
def send(self):
|
def send_heartbeat(self):
|
||||||
if not self.api_key:
|
if not self.api_key:
|
||||||
print('[WakaTime] Error: missing api key.')
|
log(ERROR, 'missing api key.')
|
||||||
return
|
return
|
||||||
ua = 'sublime/%d sublime-wakatime/%s' % (ST_VERSION, __version__)
|
ua = 'sublime/%d sublime-wakatime/%s' % (ST_VERSION, __version__)
|
||||||
cmd = [
|
cmd = [
|
||||||
@ -177,48 +402,109 @@ class SendActionThread(threading.Thread):
|
|||||||
]
|
]
|
||||||
if self.is_write:
|
if self.is_write:
|
||||||
cmd.append('--write')
|
cmd.append('--write')
|
||||||
if self.project:
|
if self.project and self.project.get('name'):
|
||||||
cmd.extend(['--project', self.project])
|
cmd.extend(['--alternate-project', self.project.get('name')])
|
||||||
elif self.folders:
|
elif self.folders:
|
||||||
project_name = find_project_name_from_folders(self.folders)
|
project_name = find_project_from_folders(self.folders, self.target_file)
|
||||||
if project_name:
|
if project_name:
|
||||||
cmd.extend(['--project', project_name])
|
cmd.extend(['--alternate-project', project_name])
|
||||||
|
if self.cursorpos is not None:
|
||||||
|
cmd.extend(['--cursorpos', '{0}'.format(self.cursorpos)])
|
||||||
for pattern in self.ignore:
|
for pattern in self.ignore:
|
||||||
cmd.extend(['--ignore', pattern])
|
cmd.extend(['--ignore', pattern])
|
||||||
if self.debug:
|
if self.debug:
|
||||||
cmd.append('--verbose')
|
cmd.append('--verbose')
|
||||||
if HAS_SSL:
|
if python_binary():
|
||||||
if self.debug:
|
cmd.insert(0, python_binary())
|
||||||
print('[WakaTime] %s' % ' '.join(cmd))
|
log(DEBUG, ' '.join(obfuscate_apikey(cmd)))
|
||||||
code = wakatime.main(cmd)
|
try:
|
||||||
if code != 0:
|
if not self.debug:
|
||||||
print('[WakaTime] Error: Response code %d from wakatime package.' % code)
|
Popen(cmd)
|
||||||
else:
|
self.sent()
|
||||||
python = python_binary()
|
|
||||||
if python:
|
|
||||||
cmd.insert(0, python)
|
|
||||||
if self.debug:
|
|
||||||
print('[WakaTime] %s' % ' '.join(cmd))
|
|
||||||
if platform.system() == 'Windows':
|
|
||||||
Popen(cmd, shell=False)
|
|
||||||
else:
|
else:
|
||||||
with open(join(expanduser('~'), '.wakatime.log'), 'a') as stderr:
|
process = Popen(cmd, stdout=PIPE, stderr=STDOUT)
|
||||||
Popen(cmd, stderr=stderr)
|
output, err = process.communicate()
|
||||||
else:
|
output = u(output)
|
||||||
print('[WakaTime] Error: Unable to find python binary.')
|
retcode = process.poll()
|
||||||
|
if (not retcode or retcode == 102) and not output:
|
||||||
|
self.sent()
|
||||||
|
if retcode:
|
||||||
|
log(DEBUG if retcode == 102 else ERROR, 'wakatime-core exited with status: {0}'.format(retcode))
|
||||||
|
if output:
|
||||||
|
log(ERROR, u('wakatime-core output: {0}').format(output))
|
||||||
|
except:
|
||||||
|
log(ERROR, u(sys.exc_info()[1]))
|
||||||
|
else:
|
||||||
|
log(ERROR, 'Unable to find python binary.')
|
||||||
|
|
||||||
|
def sent(self):
|
||||||
|
sublime.set_timeout(self.set_status_bar, 0)
|
||||||
|
sublime.set_timeout(self.set_last_heartbeat, 0)
|
||||||
|
|
||||||
|
def set_status_bar(self):
|
||||||
|
if SETTINGS.get('status_bar_message'):
|
||||||
|
self.view.set_status('wakatime', datetime.now().strftime(SETTINGS.get('status_bar_message_fmt')))
|
||||||
|
|
||||||
|
def set_last_heartbeat(self):
|
||||||
|
global LAST_HEARTBEAT
|
||||||
|
LAST_HEARTBEAT = {
|
||||||
|
'file': self.target_file,
|
||||||
|
'time': self.timestamp,
|
||||||
|
'is_write': self.is_write,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class DownloadPython(threading.Thread):
|
||||||
|
"""Non-blocking thread for extracting embeddable Python on Windows machines.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
log(INFO, 'Downloading embeddable Python...')
|
||||||
|
|
||||||
|
ver = '3.5.0'
|
||||||
|
arch = 'amd64' if platform.architecture()[0] == '64bit' else 'win32'
|
||||||
|
url = 'https://www.python.org/ftp/python/{ver}/python-{ver}-embed-{arch}.zip'.format(
|
||||||
|
ver=ver,
|
||||||
|
arch=arch,
|
||||||
|
)
|
||||||
|
|
||||||
|
if not os.path.exists(os.path.join(os.path.expanduser('~'), '.wakatime')):
|
||||||
|
os.makedirs(os.path.join(os.path.expanduser('~'), '.wakatime'))
|
||||||
|
|
||||||
|
zip_file = os.path.join(os.path.expanduser('~'), '.wakatime', 'python.zip')
|
||||||
|
try:
|
||||||
|
urllib.urlretrieve(url, zip_file)
|
||||||
|
except AttributeError:
|
||||||
|
urllib.request.urlretrieve(url, zip_file)
|
||||||
|
|
||||||
|
log(INFO, 'Extracting Python...')
|
||||||
|
with ZipFile(zip_file) as zf:
|
||||||
|
path = os.path.join(os.path.expanduser('~'), '.wakatime', 'python')
|
||||||
|
zf.extractall(path)
|
||||||
|
|
||||||
|
try:
|
||||||
|
os.remove(zip_file)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
log(INFO, 'Finished extracting Python.')
|
||||||
|
|
||||||
|
|
||||||
def plugin_loaded():
|
def plugin_loaded():
|
||||||
global SETTINGS
|
global SETTINGS
|
||||||
print('[WakaTime] Initializing WakaTime plugin v%s' % __version__)
|
log(INFO, 'Initializing WakaTime plugin v%s' % __version__)
|
||||||
|
|
||||||
if not HAS_SSL:
|
SETTINGS = sublime.load_settings(SETTINGS_FILE)
|
||||||
python = python_binary()
|
|
||||||
if not python:
|
if not python_binary():
|
||||||
|
log(WARNING, 'Python binary not found.')
|
||||||
|
if platform.system() == 'Windows':
|
||||||
|
thread = DownloadPython()
|
||||||
|
thread.start()
|
||||||
|
else:
|
||||||
sublime.error_message("Unable to find Python binary!\nWakaTime needs Python to work correctly.\n\nGo to https://www.python.org/downloads")
|
sublime.error_message("Unable to find Python binary!\nWakaTime needs Python to work correctly.\n\nGo to https://www.python.org/downloads")
|
||||||
return
|
return
|
||||||
|
|
||||||
SETTINGS = sublime.load_settings(SETTINGS_FILE)
|
|
||||||
after_loaded()
|
after_loaded()
|
||||||
|
|
||||||
|
|
||||||
@ -235,10 +521,18 @@ if ST_VERSION < 3000:
|
|||||||
class WakatimeListener(sublime_plugin.EventListener):
|
class WakatimeListener(sublime_plugin.EventListener):
|
||||||
|
|
||||||
def on_post_save(self, view):
|
def on_post_save(self, view):
|
||||||
handle_action(view, is_write=True)
|
handle_heartbeat(view, is_write=True)
|
||||||
|
|
||||||
def on_activated(self, view):
|
def on_selection_modified(self, view):
|
||||||
handle_action(view)
|
if is_view_active(view):
|
||||||
|
handle_heartbeat(view)
|
||||||
|
|
||||||
def on_modified(self, view):
|
def on_modified(self, view):
|
||||||
handle_action(view)
|
if is_view_active(view):
|
||||||
|
handle_heartbeat(view)
|
||||||
|
|
||||||
|
|
||||||
|
class WakatimeDashboardCommand(sublime_plugin.ApplicationCommand):
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
webbrowser.open_new_tab('https://wakatime.com/dashboard')
|
||||||
|
@ -9,8 +9,15 @@
|
|||||||
|
|
||||||
// Ignore files; Files (including absolute paths) that match one of these
|
// Ignore files; Files (including absolute paths) that match one of these
|
||||||
// POSIX regular expressions will not be logged.
|
// POSIX regular expressions will not be logged.
|
||||||
"ignore": ["^/tmp/", "^/etc/", "^/var/"],
|
"ignore": ["^/tmp/", "^/etc/", "^/var/", "COMMIT_EDITMSG$", "PULLREQ_EDITMSG$", "MERGE_MSG$", "TAG_EDITMSG$"],
|
||||||
|
|
||||||
// Debug mode. Set to true for verbose logging. Defaults to false.
|
// Debug mode. Set to true for verbose logging. Defaults to false.
|
||||||
"debug": false
|
"debug": false,
|
||||||
|
|
||||||
|
// Status bar message. Set to false to hide status bar message.
|
||||||
|
// Defaults to true.
|
||||||
|
"status_bar_message": true,
|
||||||
|
|
||||||
|
// Status bar message format.
|
||||||
|
"status_bar_message_fmt": "WakaTime active %I:%M %p"
|
||||||
}
|
}
|
||||||
|
39
packages/wakatime/.gitignore
vendored
39
packages/wakatime/.gitignore
vendored
@ -1,39 +0,0 @@
|
|||||||
*.py[cod]
|
|
||||||
|
|
||||||
# C extensions
|
|
||||||
*.so
|
|
||||||
|
|
||||||
# Packages
|
|
||||||
*.egg
|
|
||||||
*.egg-info
|
|
||||||
dist
|
|
||||||
build
|
|
||||||
eggs
|
|
||||||
parts
|
|
||||||
bin
|
|
||||||
var
|
|
||||||
sdist
|
|
||||||
develop-eggs
|
|
||||||
.installed.cfg
|
|
||||||
lib
|
|
||||||
lib64
|
|
||||||
|
|
||||||
# Installer logs
|
|
||||||
pip-log.txt
|
|
||||||
|
|
||||||
# Unit test / coverage reports
|
|
||||||
.coverage
|
|
||||||
.tox
|
|
||||||
nosetests.xml
|
|
||||||
|
|
||||||
# Translations
|
|
||||||
*.mo
|
|
||||||
|
|
||||||
# Mr Developer
|
|
||||||
.mr.developer.cfg
|
|
||||||
.project
|
|
||||||
.pydevproject
|
|
||||||
|
|
||||||
virtualenv
|
|
||||||
venv
|
|
||||||
.DS_Store
|
|
@ -1,15 +0,0 @@
|
|||||||
WakaTime is written and maintained by Alan Hamlett and
|
|
||||||
various contributors:
|
|
||||||
|
|
||||||
|
|
||||||
Development Lead
|
|
||||||
----------------
|
|
||||||
|
|
||||||
- Alan Hamlett <alan.hamlett@gmail.com>
|
|
||||||
|
|
||||||
|
|
||||||
Patches and Suggestions
|
|
||||||
-----------------------
|
|
||||||
|
|
||||||
- 3onyc <3onyc@x3tech.com>
|
|
||||||
- userid <xixico@ymail.com>
|
|
@ -1,292 +0,0 @@
|
|||||||
|
|
||||||
History
|
|
||||||
-------
|
|
||||||
|
|
||||||
|
|
||||||
3.0.3 (2014-12-25)
|
|
||||||
++++++++++++++++++
|
|
||||||
|
|
||||||
- detect JavaScript frameworks from script tags in Html template files
|
|
||||||
|
|
||||||
|
|
||||||
3.0.2 (2014-12-25)
|
|
||||||
++++++++++++++++++
|
|
||||||
|
|
||||||
- detect frameworks from JavaScript and JSON files
|
|
||||||
|
|
||||||
|
|
||||||
3.0.1 (2014-12-23)
|
|
||||||
++++++++++++++++++
|
|
||||||
|
|
||||||
- handle unknown language when parsing dependencies
|
|
||||||
|
|
||||||
|
|
||||||
3.0.0 (2014-12-23)
|
|
||||||
++++++++++++++++++
|
|
||||||
|
|
||||||
- detect libraries and frameworks for C++, Java, .NET, PHP, and Python files
|
|
||||||
|
|
||||||
|
|
||||||
2.1.11 (2014-12-22)
|
|
||||||
+++++++++++++++++++
|
|
||||||
|
|
||||||
- fix offline logging when response from api is None
|
|
||||||
|
|
||||||
|
|
||||||
2.1.10 (2014-12-15)
|
|
||||||
+++++++++++++++++++
|
|
||||||
|
|
||||||
- prevent queuing offline heartbeats which will never be valid (400 errors)
|
|
||||||
|
|
||||||
|
|
||||||
2.1.9 (2014-12-05)
|
|
||||||
++++++++++++++++++
|
|
||||||
|
|
||||||
- fix bug preventing offline heartbeats from being purged after uploaded
|
|
||||||
|
|
||||||
|
|
||||||
2.1.8 (2014-12-04)
|
|
||||||
++++++++++++++++++
|
|
||||||
|
|
||||||
- fix UnicodeDecodeError when building user agent string
|
|
||||||
- handle case where response is None
|
|
||||||
|
|
||||||
|
|
||||||
2.1.7 (2014-11-30)
|
|
||||||
++++++++++++++++++
|
|
||||||
|
|
||||||
- upgrade pygments to v2.0.1
|
|
||||||
- always log an error when api key is incorrect
|
|
||||||
|
|
||||||
|
|
||||||
2.1.6 (2014-11-18)
|
|
||||||
++++++++++++++++++
|
|
||||||
|
|
||||||
- fix list index error when detecting subversion project
|
|
||||||
|
|
||||||
|
|
||||||
2.1.5 (2014-11-17)
|
|
||||||
++++++++++++++++++
|
|
||||||
|
|
||||||
- catch exceptions when getting current machine time zone
|
|
||||||
|
|
||||||
|
|
||||||
2.1.4 (2014-11-12)
|
|
||||||
++++++++++++++++++
|
|
||||||
|
|
||||||
- when Python was not compiled with https support, log an error to the log file
|
|
||||||
|
|
||||||
|
|
||||||
2.1.3 (2014-11-10)
|
|
||||||
++++++++++++++++++
|
|
||||||
|
|
||||||
- correctly detect branch name for subversion projects
|
|
||||||
|
|
||||||
|
|
||||||
2.1.2 (2014-10-07)
|
|
||||||
++++++++++++++++++
|
|
||||||
|
|
||||||
- still log heartbeat when something goes wrong while reading num lines in file
|
|
||||||
|
|
||||||
|
|
||||||
2.1.1 (2014-09-30)
|
|
||||||
++++++++++++++++++
|
|
||||||
|
|
||||||
- fix bug where binary file opened as utf-8
|
|
||||||
|
|
||||||
|
|
||||||
2.1.0 (2014-09-30)
|
|
||||||
++++++++++++++++++
|
|
||||||
|
|
||||||
- python3 compatibility changes
|
|
||||||
|
|
||||||
|
|
||||||
2.0.8 (2014-08-29)
|
|
||||||
++++++++++++++++++
|
|
||||||
|
|
||||||
- supress output from svn command
|
|
||||||
|
|
||||||
|
|
||||||
2.0.7 (2014-08-27)
|
|
||||||
++++++++++++++++++
|
|
||||||
|
|
||||||
- find svn binary location from common install directories
|
|
||||||
|
|
||||||
|
|
||||||
2.0.6 (2014-08-07)
|
|
||||||
++++++++++++++++++
|
|
||||||
|
|
||||||
- encode json data as str when passing to urllib
|
|
||||||
|
|
||||||
|
|
||||||
2.0.5 (2014-07-25)
|
|
||||||
++++++++++++++++++
|
|
||||||
|
|
||||||
- option in .wakatime.cfg to obfuscate file names
|
|
||||||
|
|
||||||
|
|
||||||
2.0.4 (2014-07-25)
|
|
||||||
++++++++++++++++++
|
|
||||||
|
|
||||||
- use unique logger namespace to prevent collisions in shared plugin environments
|
|
||||||
|
|
||||||
|
|
||||||
2.0.3 (2014-06-18)
|
|
||||||
++++++++++++++++++
|
|
||||||
|
|
||||||
- use project from command line arg when no revision control project is found
|
|
||||||
|
|
||||||
|
|
||||||
2.0.2 (2014-06-09)
|
|
||||||
++++++++++++++++++
|
|
||||||
|
|
||||||
- include python3.2 compatible versions of simplejson, pytz, and tzlocal
|
|
||||||
- disable offline logging when Python was not compiled with sqlite3 module
|
|
||||||
|
|
||||||
|
|
||||||
2.0.1 (2014-05-26)
|
|
||||||
++++++++++++++++++
|
|
||||||
|
|
||||||
- fix bug in queue preventing actions with NULL values from being purged
|
|
||||||
|
|
||||||
|
|
||||||
2.0.0 (2014-05-25)
|
|
||||||
++++++++++++++++++
|
|
||||||
|
|
||||||
- offline time logging using sqlite3 to queue editor events
|
|
||||||
|
|
||||||
|
|
||||||
1.0.2 (2014-05-06)
|
|
||||||
++++++++++++++++++
|
|
||||||
|
|
||||||
- ability to set project from command line argument
|
|
||||||
|
|
||||||
|
|
||||||
1.0.1 (2014-03-05)
|
|
||||||
++++++++++++++++++
|
|
||||||
|
|
||||||
- use new domain name wakatime.com
|
|
||||||
|
|
||||||
|
|
||||||
1.0.0 (2014-02-05)
|
|
||||||
++++++++++++++++++
|
|
||||||
|
|
||||||
- detect project name and branch name from mercurial revision control
|
|
||||||
|
|
||||||
|
|
||||||
0.5.3 (2014-01-15)
|
|
||||||
++++++++++++++++++
|
|
||||||
|
|
||||||
- bug fix for unicode in Python3
|
|
||||||
|
|
||||||
|
|
||||||
0.5.2 (2014-01-14)
|
|
||||||
++++++++++++++++++
|
|
||||||
|
|
||||||
- minor bug fix for Subversion on non-English systems
|
|
||||||
|
|
||||||
|
|
||||||
0.5.1 (2013-12-13)
|
|
||||||
++++++++++++++++++
|
|
||||||
|
|
||||||
- second line in .wakatime-project file now sets branch name
|
|
||||||
|
|
||||||
|
|
||||||
0.5.0 (2013-12-13)
|
|
||||||
++++++++++++++++++
|
|
||||||
|
|
||||||
- Convert ~/.wakatime.conf to ~/.wakatime.cfg and use configparser format
|
|
||||||
- new [projectmap] section in cfg file for naming projects based on folders
|
|
||||||
|
|
||||||
|
|
||||||
0.4.10 (2013-11-13)
|
|
||||||
+++++++++++++++++++
|
|
||||||
|
|
||||||
- Placing .wakatime-project file in a folder will read the project's name from that file
|
|
||||||
|
|
||||||
|
|
||||||
0.4.9 (2013-10-27)
|
|
||||||
++++++++++++++++++
|
|
||||||
|
|
||||||
- New config for ignoring files from regular expressions
|
|
||||||
- Parse more options from config file (verbose, logfile, ignore)
|
|
||||||
|
|
||||||
|
|
||||||
0.4.8 (2013-10-13)
|
|
||||||
++++++++++++++++++
|
|
||||||
|
|
||||||
- Read git HEAD file to find current branch instead of running git command line
|
|
||||||
|
|
||||||
|
|
||||||
0.4.7 (2013-09-30)
|
|
||||||
++++++++++++++++++
|
|
||||||
|
|
||||||
- Sending local olson timezone string in api request
|
|
||||||
|
|
||||||
|
|
||||||
0.4.6 (2013-09-22)
|
|
||||||
++++++++++++++++++
|
|
||||||
|
|
||||||
- Sending total lines in file and language name to api
|
|
||||||
|
|
||||||
|
|
||||||
0.4.5 (2013-09-07)
|
|
||||||
++++++++++++++++++
|
|
||||||
|
|
||||||
- Fixed relative import error by adding packages directory to sys path
|
|
||||||
|
|
||||||
|
|
||||||
0.4.4 (2013-09-06)
|
|
||||||
++++++++++++++++++
|
|
||||||
|
|
||||||
- Using urllib2 again because of intermittent problems sending json with requests library
|
|
||||||
|
|
||||||
|
|
||||||
0.4.3 (2013-09-04)
|
|
||||||
++++++++++++++++++
|
|
||||||
|
|
||||||
- Encoding json as utf-8 before making request
|
|
||||||
|
|
||||||
|
|
||||||
0.4.2 (2013-09-04)
|
|
||||||
++++++++++++++++++
|
|
||||||
|
|
||||||
- Using requests package v1.2.3 from pypi
|
|
||||||
|
|
||||||
|
|
||||||
0.4.1 (2013-08-25)
|
|
||||||
++++++++++++++++++
|
|
||||||
|
|
||||||
- Fix bug causing requests library to omit POST content
|
|
||||||
|
|
||||||
|
|
||||||
0.4.0 (2013-08-15)
|
|
||||||
++++++++++++++++++
|
|
||||||
|
|
||||||
- Sending single branch instead of multiple tags
|
|
||||||
|
|
||||||
|
|
||||||
0.3.1 (2013-08-08)
|
|
||||||
++++++++++++++++++
|
|
||||||
|
|
||||||
- Using requests module instead of urllib2 to verify SSL certs
|
|
||||||
|
|
||||||
|
|
||||||
0.3.0 (2013-08-08)
|
|
||||||
++++++++++++++++++
|
|
||||||
|
|
||||||
- Allow importing directly from Python plugins
|
|
||||||
|
|
||||||
|
|
||||||
0.1.1 (2013-07-07)
|
|
||||||
++++++++++++++++++
|
|
||||||
|
|
||||||
- Refactored
|
|
||||||
- Simplified action events schema
|
|
||||||
|
|
||||||
|
|
||||||
0.0.1 (2013-07-05)
|
|
||||||
++++++++++++++++++
|
|
||||||
|
|
||||||
- Birth
|
|
||||||
|
|
@ -1,31 +0,0 @@
|
|||||||
BSD 3-Clause License
|
|
||||||
|
|
||||||
Copyright (c) 2014 by the respective authors (see AUTHORS file).
|
|
||||||
All rights reserved.
|
|
||||||
|
|
||||||
Redistribution and use in source and binary forms, with or without
|
|
||||||
modification, are permitted provided that the following conditions are met:
|
|
||||||
|
|
||||||
* Redistributions of source code must retain the above copyright
|
|
||||||
notice, this list of conditions and the following disclaimer.
|
|
||||||
|
|
||||||
* Redistributions in binary form must reproduce the above copyright
|
|
||||||
notice, this list of conditions and the following disclaimer
|
|
||||||
in the documentation and/or other materials provided
|
|
||||||
with the distribution.
|
|
||||||
|
|
||||||
* Neither the names of WakaTime, nor the names of its
|
|
||||||
contributors may be used to endorse or promote products derived
|
|
||||||
from this software without specific prior written permission.
|
|
||||||
|
|
||||||
THIS SOFTWARE AND DOCUMENTATION IS PROVIDED BY THE COPYRIGHT HOLDERS AND
|
|
||||||
CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT
|
|
||||||
NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
|
||||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER
|
|
||||||
OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
|
||||||
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
|
||||||
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
|
||||||
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
|
||||||
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
|
||||||
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
|
||||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@ -1,2 +0,0 @@
|
|||||||
include README.rst LICENSE HISTORY.rst
|
|
||||||
recursive-include wakatime *.py
|
|
@ -1,20 +0,0 @@
|
|||||||
WakaTime
|
|
||||||
========
|
|
||||||
|
|
||||||
Fully automatic time tracking for programmers.
|
|
||||||
|
|
||||||
This is the common interface for the WakaTime api. You shouldn't need to directly use this package unless you are creating a new plugin or your text editor's plugin asks you to install the wakatime-cli interface.
|
|
||||||
|
|
||||||
Go to http://wakatime.com to install the plugin for your text editor.
|
|
||||||
|
|
||||||
|
|
||||||
Installation
|
|
||||||
------------
|
|
||||||
|
|
||||||
pip install wakatime
|
|
||||||
|
|
||||||
|
|
||||||
Usage
|
|
||||||
-----
|
|
||||||
|
|
||||||
https://wakatime.com/
|
|
9
packages/wakatime/__about__.py
Normal file
9
packages/wakatime/__about__.py
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
__title__ = 'wakatime'
|
||||||
|
__description__ = 'Common interface to the WakaTime api.'
|
||||||
|
__url__ = 'https://github.com/wakatime/wakatime'
|
||||||
|
__version_info__ = ('4', '1', '13')
|
||||||
|
__version__ = '.'.join(__version_info__)
|
||||||
|
__author__ = 'Alan Hamlett'
|
||||||
|
__author_email__ = 'alan@wakatime.com'
|
||||||
|
__license__ = 'BSD'
|
||||||
|
__copyright__ = 'Copyright 2016 Alan Hamlett'
|
17
packages/wakatime/__init__.py
Normal file
17
packages/wakatime/__init__.py
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
wakatime
|
||||||
|
~~~~~~~~
|
||||||
|
|
||||||
|
Common interface to the WakaTime api.
|
||||||
|
http://wakatime.com
|
||||||
|
|
||||||
|
:copyright: (c) 2013 Alan Hamlett.
|
||||||
|
:license: BSD, see LICENSE for more details.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ['main']
|
||||||
|
|
||||||
|
|
||||||
|
from .main import execute
|
35
packages/wakatime/cli.py
Normal file
35
packages/wakatime/cli.py
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
wakatime.cli
|
||||||
|
~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Command-line entry point.
|
||||||
|
|
||||||
|
:copyright: (c) 2013 Alan Hamlett.
|
||||||
|
:license: BSD, see LICENSE for more details.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
# get path to local wakatime package
|
||||||
|
package_folder = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
|
||||||
|
# add local wakatime package to sys.path
|
||||||
|
sys.path.insert(0, package_folder)
|
||||||
|
|
||||||
|
# import local wakatime package
|
||||||
|
try:
|
||||||
|
import wakatime
|
||||||
|
except (TypeError, ImportError):
|
||||||
|
# on Windows, non-ASCII characters in import path can be fixed using
|
||||||
|
# the script path from sys.argv[0].
|
||||||
|
# More info at https://github.com/wakatime/wakatime/issues/32
|
||||||
|
package_folder = os.path.dirname(os.path.dirname(os.path.abspath(sys.argv[0])))
|
||||||
|
sys.path.insert(0, package_folder)
|
||||||
|
import wakatime
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
sys.exit(wakatime.execute(sys.argv[1:]))
|
@ -10,7 +10,6 @@
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import codecs
|
import codecs
|
||||||
import io
|
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
|
||||||
@ -18,32 +17,49 @@ is_py2 = (sys.version_info[0] == 2)
|
|||||||
is_py3 = (sys.version_info[0] == 3)
|
is_py3 = (sys.version_info[0] == 3)
|
||||||
|
|
||||||
|
|
||||||
if is_py2:
|
if is_py2: # pragma: nocover
|
||||||
|
|
||||||
def u(text):
|
def u(text):
|
||||||
|
if text is None:
|
||||||
|
return None
|
||||||
try:
|
try:
|
||||||
return text.decode('utf-8')
|
return text.decode('utf-8')
|
||||||
except:
|
except:
|
||||||
try:
|
try:
|
||||||
return unicode(text)
|
return text.decode(sys.getdefaultencoding())
|
||||||
except:
|
except:
|
||||||
return text
|
try:
|
||||||
|
return unicode(text)
|
||||||
|
except:
|
||||||
|
return text
|
||||||
open = codecs.open
|
open = codecs.open
|
||||||
basestring = basestring
|
basestring = basestring
|
||||||
|
|
||||||
|
|
||||||
elif is_py3:
|
elif is_py3: # pragma: nocover
|
||||||
|
|
||||||
def u(text):
|
def u(text):
|
||||||
|
if text is None:
|
||||||
|
return None
|
||||||
if isinstance(text, bytes):
|
if isinstance(text, bytes):
|
||||||
return text.decode('utf-8')
|
try:
|
||||||
return str(text)
|
return text.decode('utf-8')
|
||||||
|
except:
|
||||||
|
try:
|
||||||
|
return text.decode(sys.getdefaultencoding())
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
return str(text)
|
||||||
|
except:
|
||||||
|
return text
|
||||||
open = open
|
open = open
|
||||||
basestring = (str, bytes)
|
basestring = (str, bytes)
|
||||||
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from importlib import import_module
|
from importlib import import_module
|
||||||
except ImportError:
|
except ImportError: # pragma: nocover
|
||||||
def _resolve_name(name, package, level):
|
def _resolve_name(name, package, level):
|
||||||
"""Return the absolute name of the module to be imported."""
|
"""Return the absolute name of the module to be imported."""
|
||||||
if not hasattr(package, 'rindex'):
|
if not hasattr(package, 'rindex'):
|
17
packages/wakatime/constants.py
Normal file
17
packages/wakatime/constants.py
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
wakatime.constants
|
||||||
|
~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Constant variable definitions.
|
||||||
|
|
||||||
|
:copyright: (c) 2016 Alan Hamlett.
|
||||||
|
:license: BSD, see LICENSE for more details.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
SUCCESS = 0
|
||||||
|
API_ERROR = 102
|
||||||
|
CONFIG_FILE_PARSE_ERROR = 103
|
||||||
|
AUTH_ERROR = 104
|
||||||
|
UNKNOWN_ERROR = 105
|
@ -1,7 +1,7 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""
|
"""
|
||||||
wakatime.languages
|
wakatime.dependencies
|
||||||
~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
Parse dependencies from a source code file.
|
Parse dependencies from a source code file.
|
||||||
|
|
||||||
@ -10,9 +10,12 @@
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
from ..compat import u, open, import_module
|
from ..compat import u, open, import_module
|
||||||
|
from ..exceptions import NotYetImplemented
|
||||||
|
|
||||||
|
|
||||||
log = logging.getLogger('WakaTime')
|
log = logging.getLogger('WakaTime')
|
||||||
@ -23,26 +26,28 @@ class TokenParser(object):
|
|||||||
language, inherit from this class and implement the :meth:`parse` method
|
language, inherit from this class and implement the :meth:`parse` method
|
||||||
to return a list of dependency strings.
|
to return a list of dependency strings.
|
||||||
"""
|
"""
|
||||||
source_file = None
|
exclude = []
|
||||||
lexer = None
|
|
||||||
dependencies = []
|
|
||||||
tokens = []
|
|
||||||
|
|
||||||
def __init__(self, source_file, lexer=None):
|
def __init__(self, source_file, lexer=None):
|
||||||
|
self._tokens = None
|
||||||
|
self.dependencies = []
|
||||||
self.source_file = source_file
|
self.source_file = source_file
|
||||||
self.lexer = lexer
|
self.lexer = lexer
|
||||||
|
self.exclude = [re.compile(x, re.IGNORECASE) for x in self.exclude]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def tokens(self):
|
||||||
|
if self._tokens is None:
|
||||||
|
self._tokens = self._extract_tokens()
|
||||||
|
return self._tokens
|
||||||
|
|
||||||
def parse(self, tokens=[]):
|
def parse(self, tokens=[]):
|
||||||
""" Should return a list of dependencies.
|
""" Should return a list of dependencies.
|
||||||
"""
|
"""
|
||||||
if not tokens and not self.tokens:
|
raise NotYetImplemented()
|
||||||
self.tokens = self._extract_tokens()
|
|
||||||
raise Exception('Not yet implemented.')
|
|
||||||
|
|
||||||
def append(self, dep, truncate=False, separator=None, truncate_to=None,
|
def append(self, dep, truncate=False, separator=None, truncate_to=None,
|
||||||
strip_whitespace=True):
|
strip_whitespace=True):
|
||||||
if dep == 'as':
|
|
||||||
print('***************** as')
|
|
||||||
self._save_dependency(
|
self._save_dependency(
|
||||||
dep,
|
dep,
|
||||||
truncate=truncate,
|
truncate=truncate,
|
||||||
@ -51,10 +56,21 @@ class TokenParser(object):
|
|||||||
strip_whitespace=strip_whitespace,
|
strip_whitespace=strip_whitespace,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def partial(self, token):
|
||||||
|
return u(token).split('.')[-1]
|
||||||
|
|
||||||
def _extract_tokens(self):
|
def _extract_tokens(self):
|
||||||
if self.lexer:
|
if self.lexer:
|
||||||
with open(self.source_file, 'r', encoding='utf-8') as fh:
|
try:
|
||||||
return self.lexer.get_tokens_unprocessed(fh.read(512000))
|
with open(self.source_file, 'r', encoding='utf-8') as fh:
|
||||||
|
return self.lexer.get_tokens_unprocessed(fh.read(512000))
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
with open(self.source_file, 'r', encoding=sys.getfilesystemencoding()) as fh:
|
||||||
|
return self.lexer.get_tokens_unprocessed(fh.read(512000))
|
||||||
|
except:
|
||||||
|
pass
|
||||||
return []
|
return []
|
||||||
|
|
||||||
def _save_dependency(self, dep, truncate=False, separator=None,
|
def _save_dependency(self, dep, truncate=False, separator=None,
|
||||||
@ -64,13 +80,21 @@ class TokenParser(object):
|
|||||||
separator = u('.')
|
separator = u('.')
|
||||||
separator = u(separator)
|
separator = u(separator)
|
||||||
dep = dep.split(separator)
|
dep = dep.split(separator)
|
||||||
if truncate_to is None or truncate_to < 0 or truncate_to > len(dep) - 1:
|
if truncate_to is None or truncate_to < 1:
|
||||||
truncate_to = len(dep) - 1
|
truncate_to = 1
|
||||||
dep = dep[0] if len(dep) == 1 else separator.join(dep[0:truncate_to])
|
if truncate_to > len(dep):
|
||||||
|
truncate_to = len(dep)
|
||||||
|
dep = dep[0] if len(dep) == 1 else separator.join(dep[:truncate_to])
|
||||||
if strip_whitespace:
|
if strip_whitespace:
|
||||||
dep = dep.strip()
|
dep = dep.strip()
|
||||||
if dep:
|
if dep and (not separator or not dep.startswith(separator)):
|
||||||
self.dependencies.append(dep)
|
should_exclude = False
|
||||||
|
for compiled in self.exclude:
|
||||||
|
if compiled.search(dep):
|
||||||
|
should_exclude = True
|
||||||
|
break
|
||||||
|
if not should_exclude:
|
||||||
|
self.dependencies.append(dep)
|
||||||
|
|
||||||
|
|
||||||
class DependencyParser(object):
|
class DependencyParser(object):
|
||||||
@ -83,7 +107,7 @@ class DependencyParser(object):
|
|||||||
self.lexer = lexer
|
self.lexer = lexer
|
||||||
|
|
||||||
if self.lexer:
|
if self.lexer:
|
||||||
module_name = self.lexer.__module__.split('.')[-1]
|
module_name = self.lexer.__module__.rsplit('.', 1)[-1]
|
||||||
class_name = self.lexer.__class__.__name__.replace('Lexer', 'Parser', 1)
|
class_name = self.lexer.__class__.__name__.replace('Lexer', 'Parser', 1)
|
||||||
else:
|
else:
|
||||||
module_name = 'unknown'
|
module_name = 'unknown'
|
68
packages/wakatime/dependencies/c_cpp.py
Normal file
68
packages/wakatime/dependencies/c_cpp.py
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
wakatime.languages.c_cpp
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Parse dependencies from C++ code.
|
||||||
|
|
||||||
|
:copyright: (c) 2014 Alan Hamlett.
|
||||||
|
:license: BSD, see LICENSE for more details.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from . import TokenParser
|
||||||
|
|
||||||
|
|
||||||
|
class CppParser(TokenParser):
|
||||||
|
exclude = [
|
||||||
|
r'^stdio\.h$',
|
||||||
|
r'^stdlib\.h$',
|
||||||
|
r'^string\.h$',
|
||||||
|
r'^time\.h$',
|
||||||
|
]
|
||||||
|
|
||||||
|
def parse(self):
|
||||||
|
for index, token, content in self.tokens:
|
||||||
|
self._process_token(token, content)
|
||||||
|
return self.dependencies
|
||||||
|
|
||||||
|
def _process_token(self, token, content):
|
||||||
|
if self.partial(token) == 'Preproc':
|
||||||
|
self._process_preproc(token, content)
|
||||||
|
else:
|
||||||
|
self._process_other(token, content)
|
||||||
|
|
||||||
|
def _process_preproc(self, token, content):
|
||||||
|
if content.strip().startswith('include ') or content.strip().startswith("include\t"):
|
||||||
|
content = content.replace('include', '', 1).strip().strip('"').strip('<').strip('>').strip()
|
||||||
|
self.append(content)
|
||||||
|
|
||||||
|
def _process_other(self, token, content):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class CParser(TokenParser):
|
||||||
|
exclude = [
|
||||||
|
r'^stdio\.h$',
|
||||||
|
r'^stdlib\.h$',
|
||||||
|
r'^string\.h$',
|
||||||
|
r'^time\.h$',
|
||||||
|
]
|
||||||
|
|
||||||
|
def parse(self):
|
||||||
|
for index, token, content in self.tokens:
|
||||||
|
self._process_token(token, content)
|
||||||
|
return self.dependencies
|
||||||
|
|
||||||
|
def _process_token(self, token, content):
|
||||||
|
if self.partial(token) == 'Preproc':
|
||||||
|
self._process_preproc(token, content)
|
||||||
|
else:
|
||||||
|
self._process_other(token, content)
|
||||||
|
|
||||||
|
def _process_preproc(self, token, content):
|
||||||
|
if content.strip().startswith('include ') or content.strip().startswith("include\t"):
|
||||||
|
content = content.replace('include', '', 1).strip().strip('"').strip('<').strip('>').strip()
|
||||||
|
self.append(content)
|
||||||
|
|
||||||
|
def _process_other(self, token, content):
|
||||||
|
pass
|
@ -26,10 +26,8 @@ class JsonParser(TokenParser):
|
|||||||
state = None
|
state = None
|
||||||
level = 0
|
level = 0
|
||||||
|
|
||||||
def parse(self, tokens=[]):
|
def parse(self):
|
||||||
self._process_file_name(os.path.basename(self.source_file))
|
self._process_file_name(os.path.basename(self.source_file))
|
||||||
if not tokens and not self.tokens:
|
|
||||||
self.tokens = self._extract_tokens()
|
|
||||||
for index, token, content in self.tokens:
|
for index, token, content in self.tokens:
|
||||||
self._process_token(token, content)
|
self._process_token(token, content)
|
||||||
return self.dependencies
|
return self.dependencies
|
64
packages/wakatime/dependencies/dotnet.py
Normal file
64
packages/wakatime/dependencies/dotnet.py
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
wakatime.languages.dotnet
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Parse dependencies from .NET code.
|
||||||
|
|
||||||
|
:copyright: (c) 2014 Alan Hamlett.
|
||||||
|
:license: BSD, see LICENSE for more details.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from . import TokenParser
|
||||||
|
from ..compat import u
|
||||||
|
|
||||||
|
|
||||||
|
class CSharpParser(TokenParser):
|
||||||
|
exclude = [
|
||||||
|
r'^system$',
|
||||||
|
r'^microsoft$',
|
||||||
|
]
|
||||||
|
state = None
|
||||||
|
buffer = u('')
|
||||||
|
|
||||||
|
def parse(self):
|
||||||
|
for index, token, content in self.tokens:
|
||||||
|
self._process_token(token, content)
|
||||||
|
return self.dependencies
|
||||||
|
|
||||||
|
def _process_token(self, token, content):
|
||||||
|
if self.partial(token) == 'Keyword':
|
||||||
|
self._process_keyword(token, content)
|
||||||
|
if self.partial(token) == 'Namespace' or self.partial(token) == 'Name':
|
||||||
|
self._process_namespace(token, content)
|
||||||
|
elif self.partial(token) == 'Punctuation':
|
||||||
|
self._process_punctuation(token, content)
|
||||||
|
else:
|
||||||
|
self._process_other(token, content)
|
||||||
|
|
||||||
|
def _process_keyword(self, token, content):
|
||||||
|
if content == 'using':
|
||||||
|
self.state = 'import'
|
||||||
|
self.buffer = u('')
|
||||||
|
|
||||||
|
def _process_namespace(self, token, content):
|
||||||
|
if self.state == 'import':
|
||||||
|
if u(content) != u('import') and u(content) != u('package') and u(content) != u('namespace') and u(content) != u('static'):
|
||||||
|
if u(content) == u(';'): # pragma: nocover
|
||||||
|
self._process_punctuation(token, content)
|
||||||
|
else:
|
||||||
|
self.buffer += u(content)
|
||||||
|
|
||||||
|
def _process_punctuation(self, token, content):
|
||||||
|
if self.state == 'import':
|
||||||
|
if u(content) == u(';'):
|
||||||
|
self.append(self.buffer, truncate=True)
|
||||||
|
self.buffer = u('')
|
||||||
|
self.state = None
|
||||||
|
elif u(content) == u('='):
|
||||||
|
self.buffer = u('')
|
||||||
|
else:
|
||||||
|
self.buffer += u(content)
|
||||||
|
|
||||||
|
def _process_other(self, token, content):
|
||||||
|
pass
|
77
packages/wakatime/dependencies/go.py
Normal file
77
packages/wakatime/dependencies/go.py
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
wakatime.languages.go
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Parse dependencies from Go code.
|
||||||
|
|
||||||
|
:copyright: (c) 2016 Alan Hamlett.
|
||||||
|
:license: BSD, see LICENSE for more details.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from . import TokenParser
|
||||||
|
|
||||||
|
|
||||||
|
class GoParser(TokenParser):
|
||||||
|
state = None
|
||||||
|
parens = 0
|
||||||
|
aliases = 0
|
||||||
|
exclude = [
|
||||||
|
r'^"fmt"$',
|
||||||
|
]
|
||||||
|
|
||||||
|
def parse(self):
|
||||||
|
for index, token, content in self.tokens:
|
||||||
|
self._process_token(token, content)
|
||||||
|
return self.dependencies
|
||||||
|
|
||||||
|
def _process_token(self, token, content):
|
||||||
|
if self.partial(token) == 'Namespace':
|
||||||
|
self._process_namespace(token, content)
|
||||||
|
elif self.partial(token) == 'Punctuation':
|
||||||
|
self._process_punctuation(token, content)
|
||||||
|
elif self.partial(token) == 'String':
|
||||||
|
self._process_string(token, content)
|
||||||
|
elif self.partial(token) == 'Text':
|
||||||
|
self._process_text(token, content)
|
||||||
|
elif self.partial(token) == 'Other':
|
||||||
|
self._process_other(token, content)
|
||||||
|
else:
|
||||||
|
self._process_misc(token, content)
|
||||||
|
|
||||||
|
def _process_namespace(self, token, content):
|
||||||
|
self.state = content
|
||||||
|
self.parens = 0
|
||||||
|
self.aliases = 0
|
||||||
|
|
||||||
|
def _process_string(self, token, content):
|
||||||
|
if self.state == 'import':
|
||||||
|
self.append(content, truncate=False)
|
||||||
|
|
||||||
|
def _process_punctuation(self, token, content):
|
||||||
|
if content == '(':
|
||||||
|
self.parens += 1
|
||||||
|
elif content == ')':
|
||||||
|
self.parens -= 1
|
||||||
|
elif content == '.':
|
||||||
|
self.aliases += 1
|
||||||
|
else:
|
||||||
|
self.state = None
|
||||||
|
|
||||||
|
def _process_text(self, token, content):
|
||||||
|
if self.state == 'import':
|
||||||
|
if content == "\n" and self.parens <= 0:
|
||||||
|
self.state = None
|
||||||
|
self.parens = 0
|
||||||
|
self.aliases = 0
|
||||||
|
else:
|
||||||
|
self.state = None
|
||||||
|
|
||||||
|
def _process_other(self, token, content):
|
||||||
|
if self.state == 'import':
|
||||||
|
self.aliases += 1
|
||||||
|
else:
|
||||||
|
self.state = None
|
||||||
|
|
||||||
|
def _process_misc(self, token, content):
|
||||||
|
self.state = None
|
96
packages/wakatime/dependencies/jvm.py
Normal file
96
packages/wakatime/dependencies/jvm.py
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
wakatime.languages.java
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Parse dependencies from Java code.
|
||||||
|
|
||||||
|
:copyright: (c) 2014 Alan Hamlett.
|
||||||
|
:license: BSD, see LICENSE for more details.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from . import TokenParser
|
||||||
|
from ..compat import u
|
||||||
|
|
||||||
|
|
||||||
|
class JavaParser(TokenParser):
|
||||||
|
exclude = [
|
||||||
|
r'^java\.',
|
||||||
|
r'^javax\.',
|
||||||
|
r'^import$',
|
||||||
|
r'^package$',
|
||||||
|
r'^namespace$',
|
||||||
|
r'^static$',
|
||||||
|
]
|
||||||
|
state = None
|
||||||
|
buffer = u('')
|
||||||
|
|
||||||
|
def parse(self):
|
||||||
|
for index, token, content in self.tokens:
|
||||||
|
self._process_token(token, content)
|
||||||
|
return self.dependencies
|
||||||
|
|
||||||
|
def _process_token(self, token, content):
|
||||||
|
if self.partial(token) == 'Namespace':
|
||||||
|
self._process_namespace(token, content)
|
||||||
|
if self.partial(token) == 'Name':
|
||||||
|
self._process_name(token, content)
|
||||||
|
elif self.partial(token) == 'Attribute':
|
||||||
|
self._process_attribute(token, content)
|
||||||
|
elif self.partial(token) == 'Operator':
|
||||||
|
self._process_operator(token, content)
|
||||||
|
else:
|
||||||
|
self._process_other(token, content)
|
||||||
|
|
||||||
|
def _process_namespace(self, token, content):
|
||||||
|
if u(content) == u('import'):
|
||||||
|
self.state = 'import'
|
||||||
|
|
||||||
|
elif self.state == 'import':
|
||||||
|
keywords = [
|
||||||
|
u('package'),
|
||||||
|
u('namespace'),
|
||||||
|
u('static'),
|
||||||
|
]
|
||||||
|
if u(content) in keywords:
|
||||||
|
return
|
||||||
|
self.buffer = u('{0}{1}').format(self.buffer, u(content))
|
||||||
|
|
||||||
|
elif self.state == 'import-finished':
|
||||||
|
content = content.split(u('.'))
|
||||||
|
|
||||||
|
if len(content) == 1:
|
||||||
|
self.append(content[0])
|
||||||
|
|
||||||
|
elif len(content) > 1:
|
||||||
|
if len(content[0]) == 3:
|
||||||
|
content = content[1:]
|
||||||
|
if content[-1] == u('*'):
|
||||||
|
content = content[:len(content) - 1]
|
||||||
|
|
||||||
|
if len(content) == 1:
|
||||||
|
self.append(content[0])
|
||||||
|
elif len(content) > 1:
|
||||||
|
self.append(u('.').join(content[:2]))
|
||||||
|
|
||||||
|
self.state = None
|
||||||
|
|
||||||
|
def _process_name(self, token, content):
|
||||||
|
if self.state == 'import':
|
||||||
|
self.buffer = u('{0}{1}').format(self.buffer, u(content))
|
||||||
|
|
||||||
|
def _process_attribute(self, token, content):
|
||||||
|
if self.state == 'import':
|
||||||
|
self.buffer = u('{0}{1}').format(self.buffer, u(content))
|
||||||
|
|
||||||
|
def _process_operator(self, token, content):
|
||||||
|
if u(content) == u(';'):
|
||||||
|
self.state = 'import-finished'
|
||||||
|
self._process_namespace(token, self.buffer)
|
||||||
|
self.state = None
|
||||||
|
self.buffer = u('')
|
||||||
|
elif self.state == 'import':
|
||||||
|
self.buffer = u('{0}{1}').format(self.buffer, u(content))
|
||||||
|
|
||||||
|
def _process_other(self, token, content):
|
||||||
|
pass
|
@ -17,15 +17,13 @@ class PhpParser(TokenParser):
|
|||||||
state = None
|
state = None
|
||||||
parens = 0
|
parens = 0
|
||||||
|
|
||||||
def parse(self, tokens=[]):
|
def parse(self):
|
||||||
if not tokens and not self.tokens:
|
|
||||||
self.tokens = self._extract_tokens()
|
|
||||||
for index, token, content in self.tokens:
|
for index, token, content in self.tokens:
|
||||||
self._process_token(token, content)
|
self._process_token(token, content)
|
||||||
return self.dependencies
|
return self.dependencies
|
||||||
|
|
||||||
def _process_token(self, token, content):
|
def _process_token(self, token, content):
|
||||||
if u(token).split('.')[-1] == 'Keyword':
|
if self.partial(token) == 'Keyword':
|
||||||
self._process_keyword(token, content)
|
self._process_keyword(token, content)
|
||||||
elif u(token) == 'Token.Literal.String.Single' or u(token) == 'Token.Literal.String.Double':
|
elif u(token) == 'Token.Literal.String.Single' or u(token) == 'Token.Literal.String.Double':
|
||||||
self._process_literal_string(token, content)
|
self._process_literal_string(token, content)
|
||||||
@ -33,9 +31,9 @@ class PhpParser(TokenParser):
|
|||||||
self._process_name(token, content)
|
self._process_name(token, content)
|
||||||
elif u(token) == 'Token.Name.Function':
|
elif u(token) == 'Token.Name.Function':
|
||||||
self._process_function(token, content)
|
self._process_function(token, content)
|
||||||
elif u(token).split('.')[-1] == 'Punctuation':
|
elif self.partial(token) == 'Punctuation':
|
||||||
self._process_punctuation(token, content)
|
self._process_punctuation(token, content)
|
||||||
elif u(token).split('.')[-1] == 'Text':
|
elif self.partial(token) == 'Text':
|
||||||
self._process_text(token, content)
|
self._process_text(token, content)
|
||||||
else:
|
else:
|
||||||
self._process_other(token, content)
|
self._process_other(token, content)
|
||||||
@ -63,10 +61,10 @@ class PhpParser(TokenParser):
|
|||||||
|
|
||||||
def _process_literal_string(self, token, content):
|
def _process_literal_string(self, token, content):
|
||||||
if self.state == 'include':
|
if self.state == 'include':
|
||||||
if content != '"':
|
if content != '"' and content != "'":
|
||||||
content = content.strip()
|
content = content.strip()
|
||||||
if u(token) == 'Token.Literal.String.Double':
|
if u(token) == 'Token.Literal.String.Double':
|
||||||
content = u('"{0}"').format(content)
|
content = u("'{0}'").format(content)
|
||||||
self.append(content)
|
self.append(content)
|
||||||
self.state = None
|
self.state = None
|
||||||
|
|
@ -10,33 +10,30 @@
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
from . import TokenParser
|
from . import TokenParser
|
||||||
from ..compat import u
|
|
||||||
|
|
||||||
|
|
||||||
class PythonParser(TokenParser):
|
class PythonParser(TokenParser):
|
||||||
state = None
|
state = None
|
||||||
parens = 0
|
parens = 0
|
||||||
nonpackage = False
|
nonpackage = False
|
||||||
|
exclude = [
|
||||||
|
r'^os$',
|
||||||
|
r'^sys\.',
|
||||||
|
]
|
||||||
|
|
||||||
def parse(self, tokens=[]):
|
def parse(self):
|
||||||
if not tokens and not self.tokens:
|
|
||||||
self.tokens = self._extract_tokens()
|
|
||||||
for index, token, content in self.tokens:
|
for index, token, content in self.tokens:
|
||||||
self._process_token(token, content)
|
self._process_token(token, content)
|
||||||
return self.dependencies
|
return self.dependencies
|
||||||
|
|
||||||
def _process_token(self, token, content):
|
def _process_token(self, token, content):
|
||||||
if u(token).split('.')[-1] == 'Namespace':
|
if self.partial(token) == 'Namespace':
|
||||||
self._process_namespace(token, content)
|
self._process_namespace(token, content)
|
||||||
elif u(token).split('.')[-1] == 'Name':
|
elif self.partial(token) == 'Operator':
|
||||||
self._process_name(token, content)
|
|
||||||
elif u(token).split('.')[-1] == 'Word':
|
|
||||||
self._process_word(token, content)
|
|
||||||
elif u(token).split('.')[-1] == 'Operator':
|
|
||||||
self._process_operator(token, content)
|
self._process_operator(token, content)
|
||||||
elif u(token).split('.')[-1] == 'Punctuation':
|
elif self.partial(token) == 'Punctuation':
|
||||||
self._process_punctuation(token, content)
|
self._process_punctuation(token, content)
|
||||||
elif u(token).split('.')[-1] == 'Text':
|
elif self.partial(token) == 'Text':
|
||||||
self._process_text(token, content)
|
self._process_text(token, content)
|
||||||
else:
|
else:
|
||||||
self._process_other(token, content)
|
self._process_other(token, content)
|
||||||
@ -50,38 +47,6 @@ class PythonParser(TokenParser):
|
|||||||
else:
|
else:
|
||||||
self._process_import(token, content)
|
self._process_import(token, content)
|
||||||
|
|
||||||
def _process_name(self, token, content):
|
|
||||||
if self.state is not None:
|
|
||||||
if self.nonpackage:
|
|
||||||
self.nonpackage = False
|
|
||||||
else:
|
|
||||||
if self.state == 'from':
|
|
||||||
self.append(content, truncate=True, truncate_to=0)
|
|
||||||
if self.state == 'from-2' and content != 'import':
|
|
||||||
self.append(content, truncate=True, truncate_to=0)
|
|
||||||
elif self.state == 'import':
|
|
||||||
self.append(content, truncate=True, truncate_to=0)
|
|
||||||
elif self.state == 'import-2':
|
|
||||||
self.append(content, truncate=True, truncate_to=0)
|
|
||||||
else:
|
|
||||||
self.state = None
|
|
||||||
|
|
||||||
def _process_word(self, token, content):
|
|
||||||
if self.state is not None:
|
|
||||||
if self.nonpackage:
|
|
||||||
self.nonpackage = False
|
|
||||||
else:
|
|
||||||
if self.state == 'from':
|
|
||||||
self.append(content, truncate=True, truncate_to=0)
|
|
||||||
if self.state == 'from-2' and content != 'import':
|
|
||||||
self.append(content, truncate=True, truncate_to=0)
|
|
||||||
elif self.state == 'import':
|
|
||||||
self.append(content, truncate=True, truncate_to=0)
|
|
||||||
elif self.state == 'import-2':
|
|
||||||
self.append(content, truncate=True, truncate_to=0)
|
|
||||||
else:
|
|
||||||
self.state = None
|
|
||||||
|
|
||||||
def _process_operator(self, token, content):
|
def _process_operator(self, token, content):
|
||||||
if self.state is not None:
|
if self.state is not None:
|
||||||
if content == '.':
|
if content == '.':
|
||||||
@ -106,15 +71,15 @@ class PythonParser(TokenParser):
|
|||||||
def _process_import(self, token, content):
|
def _process_import(self, token, content):
|
||||||
if not self.nonpackage:
|
if not self.nonpackage:
|
||||||
if self.state == 'from':
|
if self.state == 'from':
|
||||||
self.append(content, truncate=True, truncate_to=0)
|
self.append(content, truncate=True, truncate_to=1)
|
||||||
self.state = 'from-2'
|
self.state = 'from-2'
|
||||||
elif self.state == 'from-2' and content != 'import':
|
elif self.state == 'from-2' and content != 'import':
|
||||||
self.append(content, truncate=True, truncate_to=0)
|
self.append(content, truncate=True, truncate_to=1)
|
||||||
elif self.state == 'import':
|
elif self.state == 'import':
|
||||||
self.append(content, truncate=True, truncate_to=0)
|
self.append(content, truncate=True, truncate_to=1)
|
||||||
self.state = 'import-2'
|
self.state = 'import-2'
|
||||||
elif self.state == 'import-2':
|
elif self.state == 'import-2':
|
||||||
self.append(content, truncate=True, truncate_to=0)
|
self.append(content, truncate=True, truncate_to=1)
|
||||||
else:
|
else:
|
||||||
self.state = None
|
self.state = None
|
||||||
self.nonpackage = False
|
self.nonpackage = False
|
@ -71,9 +71,7 @@ KEYWORDS = [
|
|||||||
|
|
||||||
class LassoJavascriptParser(TokenParser):
|
class LassoJavascriptParser(TokenParser):
|
||||||
|
|
||||||
def parse(self, tokens=[]):
|
def parse(self):
|
||||||
if not tokens and not self.tokens:
|
|
||||||
self.tokens = self._extract_tokens()
|
|
||||||
for index, token, content in self.tokens:
|
for index, token, content in self.tokens:
|
||||||
self._process_token(token, content)
|
self._process_token(token, content)
|
||||||
return self.dependencies
|
return self.dependencies
|
||||||
@ -99,9 +97,7 @@ class HtmlDjangoParser(TokenParser):
|
|||||||
current_attr = None
|
current_attr = None
|
||||||
current_attr_value = None
|
current_attr_value = None
|
||||||
|
|
||||||
def parse(self, tokens=[]):
|
def parse(self):
|
||||||
if not tokens and not self.tokens:
|
|
||||||
self.tokens = self._extract_tokens()
|
|
||||||
for index, token, content in self.tokens:
|
for index, token, content in self.tokens:
|
||||||
self._process_token(token, content)
|
self._process_token(token, content)
|
||||||
return self.dependencies
|
return self.dependencies
|
||||||
@ -120,7 +116,11 @@ class HtmlDjangoParser(TokenParser):
|
|||||||
|
|
||||||
def _process_tag(self, token, content):
|
def _process_tag(self, token, content):
|
||||||
if content.startswith('</') or content.startswith('/'):
|
if content.startswith('</') or content.startswith('/'):
|
||||||
self.tags.pop(0)
|
try:
|
||||||
|
self.tags.pop(0)
|
||||||
|
except IndexError:
|
||||||
|
# ignore errors from malformed markup
|
||||||
|
pass
|
||||||
self.getting_attrs = False
|
self.getting_attrs = False
|
||||||
elif content.startswith('<'):
|
elif content.startswith('<'):
|
||||||
self.tags.insert(0, content.replace('<', '', 1).strip().lower())
|
self.tags.insert(0, content.replace('<', '', 1).strip().lower())
|
@ -12,7 +12,6 @@
|
|||||||
import os
|
import os
|
||||||
|
|
||||||
from . import TokenParser
|
from . import TokenParser
|
||||||
from ..compat import u
|
|
||||||
|
|
||||||
|
|
||||||
FILES = {
|
FILES = {
|
||||||
@ -23,7 +22,7 @@ FILES = {
|
|||||||
|
|
||||||
class UnknownParser(TokenParser):
|
class UnknownParser(TokenParser):
|
||||||
|
|
||||||
def parse(self, tokens=[]):
|
def parse(self):
|
||||||
self._process_file_name(os.path.basename(self.source_file))
|
self._process_file_name(os.path.basename(self.source_file))
|
||||||
return self.dependencies
|
return self.dependencies
|
||||||
|
|
14
packages/wakatime/exceptions.py
Normal file
14
packages/wakatime/exceptions.py
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
wakatime.exceptions
|
||||||
|
~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Custom exceptions.
|
||||||
|
|
||||||
|
:copyright: (c) 2015 Alan Hamlett.
|
||||||
|
:license: BSD, see LICENSE for more details.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class NotYetImplemented(Exception):
|
||||||
|
"""This method needs to be implemented."""
|
136
packages/wakatime/logger.py
Normal file
136
packages/wakatime/logger.py
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
wakatime.logger
|
||||||
|
~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Provides the configured logger for writing JSON to the log file.
|
||||||
|
|
||||||
|
:copyright: (c) 2013 Alan Hamlett.
|
||||||
|
:license: BSD, see LICENSE for more details.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
from .compat import u
|
||||||
|
from .packages.requests.packages import urllib3
|
||||||
|
try:
|
||||||
|
from collections import OrderedDict # pragma: nocover
|
||||||
|
except ImportError: # pragma: nocover
|
||||||
|
from .packages.ordereddict import OrderedDict
|
||||||
|
try:
|
||||||
|
from .packages import simplejson as json # pragma: nocover
|
||||||
|
except (ImportError, SyntaxError): # pragma: nocover
|
||||||
|
import json
|
||||||
|
|
||||||
|
|
||||||
|
class CustomEncoder(json.JSONEncoder):
|
||||||
|
|
||||||
|
def default(self, obj):
|
||||||
|
if isinstance(obj, bytes): # pragma: nocover
|
||||||
|
obj = u(obj)
|
||||||
|
return json.dumps(obj)
|
||||||
|
try: # pragma: nocover
|
||||||
|
encoded = super(CustomEncoder, self).default(obj)
|
||||||
|
except UnicodeDecodeError: # pragma: nocover
|
||||||
|
obj = u(obj)
|
||||||
|
encoded = super(CustomEncoder, self).default(obj)
|
||||||
|
return encoded
|
||||||
|
|
||||||
|
|
||||||
|
class JsonFormatter(logging.Formatter):
|
||||||
|
|
||||||
|
def setup(self, timestamp, isWrite, entity, version, plugin, verbose,
|
||||||
|
warnings=False):
|
||||||
|
self.timestamp = timestamp
|
||||||
|
self.isWrite = isWrite
|
||||||
|
self.entity = entity
|
||||||
|
self.version = version
|
||||||
|
self.plugin = plugin
|
||||||
|
self.verbose = verbose
|
||||||
|
self.warnings = warnings
|
||||||
|
|
||||||
|
def format(self, record, *args):
|
||||||
|
data = OrderedDict([
|
||||||
|
('now', self.formatTime(record, self.datefmt)),
|
||||||
|
])
|
||||||
|
data['version'] = self.version
|
||||||
|
data['plugin'] = self.plugin
|
||||||
|
data['time'] = self.timestamp
|
||||||
|
if self.verbose:
|
||||||
|
data['caller'] = record.pathname
|
||||||
|
data['lineno'] = record.lineno
|
||||||
|
data['isWrite'] = self.isWrite
|
||||||
|
data['file'] = self.entity
|
||||||
|
if not self.isWrite:
|
||||||
|
del data['isWrite']
|
||||||
|
data['level'] = record.levelname
|
||||||
|
data['message'] = record.getMessage() if self.warnings else record.msg
|
||||||
|
if not self.plugin:
|
||||||
|
del data['plugin']
|
||||||
|
return CustomEncoder().encode(data)
|
||||||
|
|
||||||
|
|
||||||
|
def traceback_formatter(*args, **kwargs):
|
||||||
|
if 'level' in kwargs and (kwargs['level'].lower() == 'warn' or kwargs['level'].lower() == 'warning'):
|
||||||
|
logging.getLogger('WakaTime').warning(traceback.format_exc())
|
||||||
|
elif 'level' in kwargs and kwargs['level'].lower() == 'info':
|
||||||
|
logging.getLogger('WakaTime').info(traceback.format_exc())
|
||||||
|
elif 'level' in kwargs and kwargs['level'].lower() == 'debug':
|
||||||
|
logging.getLogger('WakaTime').debug(traceback.format_exc())
|
||||||
|
else:
|
||||||
|
logging.getLogger('WakaTime').error(traceback.format_exc())
|
||||||
|
|
||||||
|
|
||||||
|
def set_log_level(logger, args):
|
||||||
|
level = logging.WARN
|
||||||
|
if args.verbose:
|
||||||
|
level = logging.DEBUG
|
||||||
|
logger.setLevel(level)
|
||||||
|
|
||||||
|
|
||||||
|
def setup_logging(args, version):
|
||||||
|
urllib3.disable_warnings()
|
||||||
|
logger = logging.getLogger('WakaTime')
|
||||||
|
for handler in logger.handlers:
|
||||||
|
logger.removeHandler(handler)
|
||||||
|
set_log_level(logger, args)
|
||||||
|
logfile = args.logfile
|
||||||
|
if not logfile:
|
||||||
|
logfile = '~/.wakatime.log'
|
||||||
|
handler = logging.FileHandler(os.path.expanduser(logfile))
|
||||||
|
formatter = JsonFormatter(datefmt='%Y/%m/%d %H:%M:%S %z')
|
||||||
|
formatter.setup(
|
||||||
|
timestamp=args.timestamp,
|
||||||
|
isWrite=args.isWrite,
|
||||||
|
entity=args.entity,
|
||||||
|
version=version,
|
||||||
|
plugin=args.plugin,
|
||||||
|
verbose=args.verbose,
|
||||||
|
)
|
||||||
|
handler.setFormatter(formatter)
|
||||||
|
logger.addHandler(handler)
|
||||||
|
|
||||||
|
# add custom traceback logging method
|
||||||
|
logger.traceback = traceback_formatter
|
||||||
|
|
||||||
|
warnings_formatter = JsonFormatter(datefmt='%Y/%m/%d %H:%M:%S %z')
|
||||||
|
warnings_formatter.setup(
|
||||||
|
timestamp=args.timestamp,
|
||||||
|
isWrite=args.isWrite,
|
||||||
|
entity=args.entity,
|
||||||
|
version=version,
|
||||||
|
plugin=args.plugin,
|
||||||
|
verbose=args.verbose,
|
||||||
|
warnings=True,
|
||||||
|
)
|
||||||
|
warnings_handler = logging.FileHandler(os.path.expanduser(logfile))
|
||||||
|
warnings_handler.setFormatter(warnings_formatter)
|
||||||
|
logging.getLogger('py.warnings').addHandler(warnings_handler)
|
||||||
|
try:
|
||||||
|
logging.captureWarnings(True)
|
||||||
|
except AttributeError: # pragma: nocover
|
||||||
|
pass # Python >= 2.7 is needed to capture warnings
|
||||||
|
|
||||||
|
return logger
|
503
packages/wakatime/main.py
Normal file
503
packages/wakatime/main.py
Normal file
@ -0,0 +1,503 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
wakatime.main
|
||||||
|
~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
wakatime module entry point.
|
||||||
|
|
||||||
|
:copyright: (c) 2013 Alan Hamlett.
|
||||||
|
:license: BSD, see LICENSE for more details.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import print_function
|
||||||
|
|
||||||
|
import base64
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import platform
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
import traceback
|
||||||
|
import socket
|
||||||
|
try:
|
||||||
|
import ConfigParser as configparser
|
||||||
|
except ImportError: # pragma: nocover
|
||||||
|
import configparser
|
||||||
|
|
||||||
|
pwd = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
sys.path.insert(0, os.path.dirname(pwd))
|
||||||
|
sys.path.insert(0, os.path.join(pwd, 'packages'))
|
||||||
|
|
||||||
|
from .__about__ import __version__
|
||||||
|
from .compat import u, open, is_py3
|
||||||
|
from .constants import (
|
||||||
|
API_ERROR,
|
||||||
|
AUTH_ERROR,
|
||||||
|
CONFIG_FILE_PARSE_ERROR,
|
||||||
|
SUCCESS,
|
||||||
|
UNKNOWN_ERROR,
|
||||||
|
)
|
||||||
|
from .logger import setup_logging
|
||||||
|
from .offlinequeue import Queue
|
||||||
|
from .packages import argparse
|
||||||
|
from .packages import requests
|
||||||
|
from .packages.requests.exceptions import RequestException
|
||||||
|
from .project import get_project_info
|
||||||
|
from .session_cache import SessionCache
|
||||||
|
from .stats import get_file_stats
|
||||||
|
try:
|
||||||
|
from .packages import simplejson as json # pragma: nocover
|
||||||
|
except (ImportError, SyntaxError): # pragma: nocover
|
||||||
|
import json
|
||||||
|
try:
|
||||||
|
from .packages import tzlocal
|
||||||
|
except: # pragma: nocover
|
||||||
|
from .packages import tzlocal3 as tzlocal
|
||||||
|
|
||||||
|
|
||||||
|
log = logging.getLogger('WakaTime')
|
||||||
|
|
||||||
|
|
||||||
|
class FileAction(argparse.Action):
|
||||||
|
|
||||||
|
def __call__(self, parser, namespace, values, option_string=None):
|
||||||
|
try:
|
||||||
|
if os.path.isfile(values):
|
||||||
|
values = os.path.realpath(values)
|
||||||
|
except: # pragma: nocover
|
||||||
|
pass
|
||||||
|
setattr(namespace, self.dest, values)
|
||||||
|
|
||||||
|
|
||||||
|
def parseConfigFile(configFile=None):
|
||||||
|
"""Returns a configparser.SafeConfigParser instance with configs
|
||||||
|
read from the config file. Default location of the config file is
|
||||||
|
at ~/.wakatime.cfg.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if not configFile:
|
||||||
|
configFile = os.path.join(os.path.expanduser('~'), '.wakatime.cfg')
|
||||||
|
|
||||||
|
configs = configparser.SafeConfigParser()
|
||||||
|
try:
|
||||||
|
with open(configFile, 'r', encoding='utf-8') as fh:
|
||||||
|
try:
|
||||||
|
configs.readfp(fh)
|
||||||
|
except configparser.Error:
|
||||||
|
print(traceback.format_exc())
|
||||||
|
return None
|
||||||
|
except IOError:
|
||||||
|
print(u('Error: Could not read from config file {0}').format(u(configFile)))
|
||||||
|
return configs
|
||||||
|
|
||||||
|
|
||||||
|
def parseArguments():
|
||||||
|
"""Parse command line arguments and configs from ~/.wakatime.cfg.
|
||||||
|
Command line arguments take precedence over config file settings.
|
||||||
|
Returns instances of ArgumentParser and SafeConfigParser.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# define supported command line arguments
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description='Common interface for the WakaTime api.')
|
||||||
|
parser.add_argument('--entity', dest='entity', metavar='FILE',
|
||||||
|
action=FileAction,
|
||||||
|
help='absolute path to file for the heartbeat; can also be a '+
|
||||||
|
'url, domain, or app when --entitytype is not file')
|
||||||
|
parser.add_argument('--file', dest='file', action=FileAction,
|
||||||
|
help=argparse.SUPPRESS)
|
||||||
|
parser.add_argument('--key', dest='key',
|
||||||
|
help='your wakatime api key; uses api_key from '+
|
||||||
|
'~/.wakatime.conf by default')
|
||||||
|
parser.add_argument('--write', dest='isWrite',
|
||||||
|
action='store_true',
|
||||||
|
help='when set, tells api this heartbeat was triggered from '+
|
||||||
|
'writing to a file')
|
||||||
|
parser.add_argument('--plugin', dest='plugin',
|
||||||
|
help='optional text editor plugin name and version '+
|
||||||
|
'for User-Agent header')
|
||||||
|
parser.add_argument('--time', dest='timestamp', metavar='time',
|
||||||
|
type=float,
|
||||||
|
help='optional floating-point unix epoch timestamp; '+
|
||||||
|
'uses current time by default')
|
||||||
|
parser.add_argument('--lineno', dest='lineno',
|
||||||
|
help='optional line number; current line being edited')
|
||||||
|
parser.add_argument('--cursorpos', dest='cursorpos',
|
||||||
|
help='optional cursor position in the current file')
|
||||||
|
parser.add_argument('--entitytype', dest='entity_type',
|
||||||
|
help='entity type for this heartbeat. can be one of "file", '+
|
||||||
|
'"url", "domain", or "app"; defaults to file.')
|
||||||
|
parser.add_argument('--proxy', dest='proxy',
|
||||||
|
help='optional https proxy url; for example: '+
|
||||||
|
'https://user:pass@localhost:8080')
|
||||||
|
parser.add_argument('--project', dest='project',
|
||||||
|
help='optional project name')
|
||||||
|
parser.add_argument('--alternate-project', dest='alternate_project',
|
||||||
|
help='optional alternate project name; auto-discovered project '+
|
||||||
|
'takes priority')
|
||||||
|
parser.add_argument('--hostname', dest='hostname', help='hostname of '+
|
||||||
|
'current machine.')
|
||||||
|
parser.add_argument('--disableoffline', dest='offline',
|
||||||
|
action='store_false',
|
||||||
|
help='disables offline time logging instead of queuing logged time')
|
||||||
|
parser.add_argument('--hidefilenames', dest='hidefilenames',
|
||||||
|
action='store_true',
|
||||||
|
help='obfuscate file names; will not send file names to api')
|
||||||
|
parser.add_argument('--exclude', dest='exclude', action='append',
|
||||||
|
help='filename patterns to exclude from logging; POSIX regex '+
|
||||||
|
'syntax; can be used more than once')
|
||||||
|
parser.add_argument('--include', dest='include', action='append',
|
||||||
|
help='filename patterns to log; when used in combination with '+
|
||||||
|
'--exclude, files matching include will still be logged; '+
|
||||||
|
'POSIX regex syntax; can be used more than once')
|
||||||
|
parser.add_argument('--ignore', dest='ignore', action='append',
|
||||||
|
help=argparse.SUPPRESS)
|
||||||
|
parser.add_argument('--logfile', dest='logfile',
|
||||||
|
help='defaults to ~/.wakatime.log')
|
||||||
|
parser.add_argument('--apiurl', dest='api_url',
|
||||||
|
help='heartbeats api url; for debugging with a local server')
|
||||||
|
parser.add_argument('--timeout', dest='timeout', type=int,
|
||||||
|
help='number of seconds to wait when sending heartbeats to api')
|
||||||
|
parser.add_argument('--config', dest='config',
|
||||||
|
help='defaults to ~/.wakatime.conf')
|
||||||
|
parser.add_argument('--verbose', dest='verbose', action='store_true',
|
||||||
|
help='turns on debug messages in log file')
|
||||||
|
parser.add_argument('--version', action='version', version=__version__)
|
||||||
|
|
||||||
|
# parse command line arguments
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
# use current unix epoch timestamp by default
|
||||||
|
if not args.timestamp:
|
||||||
|
args.timestamp = time.time()
|
||||||
|
|
||||||
|
# parse ~/.wakatime.cfg file
|
||||||
|
configs = parseConfigFile(args.config)
|
||||||
|
if configs is None:
|
||||||
|
return args, configs
|
||||||
|
|
||||||
|
# update args from configs
|
||||||
|
if not args.key:
|
||||||
|
default_key = None
|
||||||
|
if configs.has_option('settings', 'api_key'):
|
||||||
|
default_key = configs.get('settings', 'api_key')
|
||||||
|
elif configs.has_option('settings', 'apikey'):
|
||||||
|
default_key = configs.get('settings', 'apikey')
|
||||||
|
if default_key:
|
||||||
|
args.key = default_key
|
||||||
|
else:
|
||||||
|
parser.error('Missing api key')
|
||||||
|
if not args.entity_type:
|
||||||
|
args.entity_type = 'file'
|
||||||
|
if not args.entity:
|
||||||
|
if args.file:
|
||||||
|
args.entity = args.file
|
||||||
|
else:
|
||||||
|
parser.error('argument --entity is required')
|
||||||
|
if not args.exclude:
|
||||||
|
args.exclude = []
|
||||||
|
if configs.has_option('settings', 'ignore'):
|
||||||
|
try:
|
||||||
|
for pattern in configs.get('settings', 'ignore').split("\n"):
|
||||||
|
if pattern.strip() != '':
|
||||||
|
args.exclude.append(pattern)
|
||||||
|
except TypeError: # pragma: nocover
|
||||||
|
pass
|
||||||
|
if configs.has_option('settings', 'exclude'):
|
||||||
|
try:
|
||||||
|
for pattern in configs.get('settings', 'exclude').split("\n"):
|
||||||
|
if pattern.strip() != '':
|
||||||
|
args.exclude.append(pattern)
|
||||||
|
except TypeError: # pragma: nocover
|
||||||
|
pass
|
||||||
|
if not args.include:
|
||||||
|
args.include = []
|
||||||
|
if configs.has_option('settings', 'include'):
|
||||||
|
try:
|
||||||
|
for pattern in configs.get('settings', 'include').split("\n"):
|
||||||
|
if pattern.strip() != '':
|
||||||
|
args.include.append(pattern)
|
||||||
|
except TypeError: # pragma: nocover
|
||||||
|
pass
|
||||||
|
if args.offline and configs.has_option('settings', 'offline'):
|
||||||
|
args.offline = configs.getboolean('settings', 'offline')
|
||||||
|
if not args.hidefilenames and configs.has_option('settings', 'hidefilenames'):
|
||||||
|
args.hidefilenames = configs.getboolean('settings', 'hidefilenames')
|
||||||
|
if not args.proxy and configs.has_option('settings', 'proxy'):
|
||||||
|
args.proxy = configs.get('settings', 'proxy')
|
||||||
|
if not args.verbose and configs.has_option('settings', 'verbose'):
|
||||||
|
args.verbose = configs.getboolean('settings', 'verbose')
|
||||||
|
if not args.verbose and configs.has_option('settings', 'debug'):
|
||||||
|
args.verbose = configs.getboolean('settings', 'debug')
|
||||||
|
if not args.logfile and configs.has_option('settings', 'logfile'):
|
||||||
|
args.logfile = configs.get('settings', 'logfile')
|
||||||
|
if not args.api_url and configs.has_option('settings', 'api_url'):
|
||||||
|
args.api_url = configs.get('settings', 'api_url')
|
||||||
|
if not args.timeout and configs.has_option('settings', 'timeout'):
|
||||||
|
try:
|
||||||
|
args.timeout = int(configs.get('settings', 'timeout'))
|
||||||
|
except ValueError:
|
||||||
|
print(traceback.format_exc())
|
||||||
|
|
||||||
|
return args, configs
|
||||||
|
|
||||||
|
|
||||||
|
def should_exclude(entity, include, exclude):
|
||||||
|
if entity is not None and entity.strip() != '':
|
||||||
|
try:
|
||||||
|
for pattern in include:
|
||||||
|
try:
|
||||||
|
compiled = re.compile(pattern, re.IGNORECASE)
|
||||||
|
if compiled.search(entity):
|
||||||
|
return False
|
||||||
|
except re.error as ex:
|
||||||
|
log.warning(u('Regex error ({msg}) for include pattern: {pattern}').format(
|
||||||
|
msg=u(ex),
|
||||||
|
pattern=u(pattern),
|
||||||
|
))
|
||||||
|
except TypeError: # pragma: nocover
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
for pattern in exclude:
|
||||||
|
try:
|
||||||
|
compiled = re.compile(pattern, re.IGNORECASE)
|
||||||
|
if compiled.search(entity):
|
||||||
|
return pattern
|
||||||
|
except re.error as ex:
|
||||||
|
log.warning(u('Regex error ({msg}) for exclude pattern: {pattern}').format(
|
||||||
|
msg=u(ex),
|
||||||
|
pattern=u(pattern),
|
||||||
|
))
|
||||||
|
except TypeError: # pragma: nocover
|
||||||
|
pass
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def get_user_agent(plugin):
|
||||||
|
ver = sys.version_info
|
||||||
|
python_version = '%d.%d.%d.%s.%d' % (ver[0], ver[1], ver[2], ver[3], ver[4])
|
||||||
|
user_agent = u('wakatime/{ver} ({platform}) Python{py_ver}').format(
|
||||||
|
ver=u(__version__),
|
||||||
|
platform=u(platform.platform()),
|
||||||
|
py_ver=python_version,
|
||||||
|
)
|
||||||
|
if plugin:
|
||||||
|
user_agent = u('{user_agent} {plugin}').format(
|
||||||
|
user_agent=user_agent,
|
||||||
|
plugin=u(plugin),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
user_agent = u('{user_agent} Unknown/0').format(
|
||||||
|
user_agent=user_agent,
|
||||||
|
)
|
||||||
|
return user_agent
|
||||||
|
|
||||||
|
|
||||||
|
def send_heartbeat(project=None, branch=None, hostname=None, stats={}, key=None,
|
||||||
|
entity=None, timestamp=None, isWrite=None, plugin=None,
|
||||||
|
offline=None, entity_type='file', hidefilenames=None,
|
||||||
|
proxy=None, api_url=None, timeout=None, **kwargs):
|
||||||
|
"""Sends heartbeat as POST request to WakaTime api server.
|
||||||
|
|
||||||
|
Returns `SUCCESS` when heartbeat was sent, otherwise returns an
|
||||||
|
error code constant.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if not api_url:
|
||||||
|
api_url = 'https://api.wakatime.com/api/v1/heartbeats'
|
||||||
|
if not timeout:
|
||||||
|
timeout = 30
|
||||||
|
log.debug('Sending heartbeat to api at %s' % api_url)
|
||||||
|
data = {
|
||||||
|
'time': timestamp,
|
||||||
|
'entity': entity,
|
||||||
|
'type': entity_type,
|
||||||
|
}
|
||||||
|
if hidefilenames and entity is not None and entity_type == 'file':
|
||||||
|
extension = u(os.path.splitext(data['entity'])[1])
|
||||||
|
data['entity'] = u('HIDDEN{0}').format(extension)
|
||||||
|
if stats.get('lines'):
|
||||||
|
data['lines'] = stats['lines']
|
||||||
|
if stats.get('language'):
|
||||||
|
data['language'] = stats['language']
|
||||||
|
if stats.get('dependencies'):
|
||||||
|
data['dependencies'] = stats['dependencies']
|
||||||
|
if stats.get('lineno'):
|
||||||
|
data['lineno'] = stats['lineno']
|
||||||
|
if stats.get('cursorpos'):
|
||||||
|
data['cursorpos'] = stats['cursorpos']
|
||||||
|
if isWrite:
|
||||||
|
data['is_write'] = isWrite
|
||||||
|
if project:
|
||||||
|
data['project'] = project
|
||||||
|
if branch:
|
||||||
|
data['branch'] = branch
|
||||||
|
log.debug(data)
|
||||||
|
|
||||||
|
# setup api request
|
||||||
|
request_body = json.dumps(data)
|
||||||
|
api_key = u(base64.b64encode(str.encode(key) if is_py3 else key))
|
||||||
|
auth = u('Basic {api_key}').format(api_key=api_key)
|
||||||
|
headers = {
|
||||||
|
'User-Agent': get_user_agent(plugin),
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Accept': 'application/json',
|
||||||
|
'Authorization': auth,
|
||||||
|
}
|
||||||
|
if hostname:
|
||||||
|
headers['X-Machine-Name'] = u(hostname).encode('utf-8')
|
||||||
|
proxies = {}
|
||||||
|
if proxy:
|
||||||
|
proxies['https'] = proxy
|
||||||
|
|
||||||
|
# add Olson timezone to request
|
||||||
|
try:
|
||||||
|
tz = tzlocal.get_localzone()
|
||||||
|
except:
|
||||||
|
tz = None
|
||||||
|
if tz:
|
||||||
|
headers['TimeZone'] = u(tz.zone).encode('utf-8')
|
||||||
|
|
||||||
|
session_cache = SessionCache()
|
||||||
|
session = session_cache.get()
|
||||||
|
|
||||||
|
# log time to api
|
||||||
|
response = None
|
||||||
|
try:
|
||||||
|
response = session.post(api_url, data=request_body, headers=headers,
|
||||||
|
proxies=proxies, timeout=timeout)
|
||||||
|
except RequestException:
|
||||||
|
exception_data = {
|
||||||
|
sys.exc_info()[0].__name__: u(sys.exc_info()[1]),
|
||||||
|
}
|
||||||
|
if log.isEnabledFor(logging.DEBUG):
|
||||||
|
exception_data['traceback'] = traceback.format_exc()
|
||||||
|
if offline:
|
||||||
|
queue = Queue()
|
||||||
|
queue.push(data, json.dumps(stats), plugin)
|
||||||
|
if log.isEnabledFor(logging.DEBUG):
|
||||||
|
log.warn(exception_data)
|
||||||
|
else:
|
||||||
|
log.error(exception_data)
|
||||||
|
else:
|
||||||
|
code = response.status_code if response is not None else None
|
||||||
|
content = response.text if response is not None else None
|
||||||
|
if code == requests.codes.created or code == requests.codes.accepted:
|
||||||
|
log.debug({
|
||||||
|
'response_code': code,
|
||||||
|
})
|
||||||
|
session_cache.save(session)
|
||||||
|
return SUCCESS
|
||||||
|
if offline:
|
||||||
|
if code != 400:
|
||||||
|
queue = Queue()
|
||||||
|
queue.push(data, json.dumps(stats), plugin)
|
||||||
|
if code == 401:
|
||||||
|
log.error({
|
||||||
|
'response_code': code,
|
||||||
|
'response_content': content,
|
||||||
|
})
|
||||||
|
session_cache.delete()
|
||||||
|
return AUTH_ERROR
|
||||||
|
elif log.isEnabledFor(logging.DEBUG):
|
||||||
|
log.warn({
|
||||||
|
'response_code': code,
|
||||||
|
'response_content': content,
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
log.error({
|
||||||
|
'response_code': code,
|
||||||
|
'response_content': content,
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
log.error({
|
||||||
|
'response_code': code,
|
||||||
|
'response_content': content,
|
||||||
|
})
|
||||||
|
session_cache.delete()
|
||||||
|
return API_ERROR
|
||||||
|
|
||||||
|
|
||||||
|
def sync_offline_heartbeats(args, hostname):
|
||||||
|
"""Sends all heartbeats which were cached in the offline Queue."""
|
||||||
|
|
||||||
|
queue = Queue()
|
||||||
|
while True:
|
||||||
|
heartbeat = queue.pop()
|
||||||
|
if heartbeat is None:
|
||||||
|
break
|
||||||
|
status = send_heartbeat(
|
||||||
|
project=heartbeat['project'],
|
||||||
|
entity=heartbeat['entity'],
|
||||||
|
timestamp=heartbeat['time'],
|
||||||
|
branch=heartbeat['branch'],
|
||||||
|
hostname=hostname,
|
||||||
|
stats=json.loads(heartbeat['stats']),
|
||||||
|
key=args.key,
|
||||||
|
isWrite=heartbeat['is_write'],
|
||||||
|
plugin=heartbeat['plugin'],
|
||||||
|
offline=args.offline,
|
||||||
|
hidefilenames=args.hidefilenames,
|
||||||
|
entity_type=heartbeat['type'],
|
||||||
|
proxy=args.proxy,
|
||||||
|
api_url=args.api_url,
|
||||||
|
timeout=args.timeout,
|
||||||
|
)
|
||||||
|
if status != SUCCESS:
|
||||||
|
if status == AUTH_ERROR:
|
||||||
|
return AUTH_ERROR
|
||||||
|
break
|
||||||
|
return SUCCESS
|
||||||
|
|
||||||
|
|
||||||
|
def execute(argv=None):
|
||||||
|
if argv:
|
||||||
|
sys.argv = ['wakatime'] + argv
|
||||||
|
|
||||||
|
args, configs = parseArguments()
|
||||||
|
if configs is None:
|
||||||
|
return CONFIG_FILE_PARSE_ERROR
|
||||||
|
|
||||||
|
setup_logging(args, __version__)
|
||||||
|
|
||||||
|
try:
|
||||||
|
exclude = should_exclude(args.entity, args.include, args.exclude)
|
||||||
|
if exclude is not False:
|
||||||
|
log.debug(u('Skipping because matches exclude pattern: {pattern}').format(
|
||||||
|
pattern=u(exclude),
|
||||||
|
))
|
||||||
|
return SUCCESS
|
||||||
|
|
||||||
|
if args.entity_type != 'file' or os.path.isfile(args.entity):
|
||||||
|
|
||||||
|
stats = get_file_stats(args.entity,
|
||||||
|
entity_type=args.entity_type,
|
||||||
|
lineno=args.lineno,
|
||||||
|
cursorpos=args.cursorpos)
|
||||||
|
|
||||||
|
project = args.project or args.alternate_project
|
||||||
|
branch = None
|
||||||
|
if args.entity_type == 'file':
|
||||||
|
project, branch = get_project_info(configs, args)
|
||||||
|
|
||||||
|
kwargs = vars(args)
|
||||||
|
kwargs['project'] = project
|
||||||
|
kwargs['branch'] = branch
|
||||||
|
kwargs['stats'] = stats
|
||||||
|
hostname = args.hostname or socket.gethostname()
|
||||||
|
kwargs['hostname'] = hostname
|
||||||
|
kwargs['timeout'] = args.timeout
|
||||||
|
|
||||||
|
status = send_heartbeat(**kwargs)
|
||||||
|
if status == SUCCESS:
|
||||||
|
return sync_offline_heartbeats(args, hostname)
|
||||||
|
else:
|
||||||
|
return status
|
||||||
|
|
||||||
|
else:
|
||||||
|
log.debug('File does not exist; ignoring this heartbeat.')
|
||||||
|
return SUCCESS
|
||||||
|
except:
|
||||||
|
log.traceback()
|
||||||
|
return UNKNOWN_ERROR
|
@ -1,10 +1,9 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""
|
"""
|
||||||
wakatime.queue
|
wakatime.offlinequeue
|
||||||
~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
Queue for offline time logging.
|
Queue for saving heartbeats while offline.
|
||||||
http://wakatime.com
|
|
||||||
|
|
||||||
:copyright: (c) 2014 Alan Hamlett.
|
:copyright: (c) 2014 Alan Hamlett.
|
||||||
:license: BSD, see LICENSE for more details.
|
:license: BSD, see LICENSE for more details.
|
||||||
@ -19,21 +18,28 @@ from time import sleep
|
|||||||
try:
|
try:
|
||||||
import sqlite3
|
import sqlite3
|
||||||
HAS_SQL = True
|
HAS_SQL = True
|
||||||
except ImportError:
|
except ImportError: # pragma: nocover
|
||||||
HAS_SQL = False
|
HAS_SQL = False
|
||||||
|
|
||||||
|
from .compat import u
|
||||||
|
|
||||||
|
|
||||||
log = logging.getLogger('WakaTime')
|
log = logging.getLogger('WakaTime')
|
||||||
|
|
||||||
|
|
||||||
class Queue(object):
|
class Queue(object):
|
||||||
DB_FILE = os.path.join(os.path.expanduser('~'), '.wakatime.db')
|
db_file = os.path.join(os.path.expanduser('~'), '.wakatime.db')
|
||||||
|
table_name = 'heartbeat_1'
|
||||||
|
|
||||||
|
def get_db_file(self):
|
||||||
|
return self.db_file
|
||||||
|
|
||||||
def connect(self):
|
def connect(self):
|
||||||
conn = sqlite3.connect(self.DB_FILE)
|
conn = sqlite3.connect(self.get_db_file())
|
||||||
c = conn.cursor()
|
c = conn.cursor()
|
||||||
c.execute('''CREATE TABLE IF NOT EXISTS heartbeat (
|
c.execute('''CREATE TABLE IF NOT EXISTS {0} (
|
||||||
file text,
|
entity text,
|
||||||
|
type text,
|
||||||
time real,
|
time real,
|
||||||
project text,
|
project text,
|
||||||
branch text,
|
branch text,
|
||||||
@ -41,34 +47,33 @@ class Queue(object):
|
|||||||
stats text,
|
stats text,
|
||||||
misc text,
|
misc text,
|
||||||
plugin text)
|
plugin text)
|
||||||
''')
|
'''.format(self.table_name))
|
||||||
return (conn, c)
|
return (conn, c)
|
||||||
|
|
||||||
|
|
||||||
def push(self, data, stats, plugin, misc=None):
|
def push(self, data, stats, plugin, misc=None):
|
||||||
if not HAS_SQL:
|
if not HAS_SQL: # pragma: nocover
|
||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
conn, c = self.connect()
|
conn, c = self.connect()
|
||||||
heartbeat = {
|
heartbeat = {
|
||||||
'file': data.get('file'),
|
'entity': u(data.get('entity')),
|
||||||
|
'type': u(data.get('type')),
|
||||||
'time': data.get('time'),
|
'time': data.get('time'),
|
||||||
'project': data.get('project'),
|
'project': u(data.get('project')),
|
||||||
'branch': data.get('branch'),
|
'branch': u(data.get('branch')),
|
||||||
'is_write': 1 if data.get('is_write') else 0,
|
'is_write': 1 if data.get('is_write') else 0,
|
||||||
'stats': stats,
|
'stats': u(stats),
|
||||||
'misc': misc,
|
'misc': u(misc),
|
||||||
'plugin': plugin,
|
'plugin': u(plugin),
|
||||||
}
|
}
|
||||||
c.execute('INSERT INTO heartbeat VALUES (:file,:time,:project,:branch,:is_write,:stats,:misc,:plugin)', heartbeat)
|
c.execute('INSERT INTO {0} VALUES (:entity,:type,:time,:project,:branch,:is_write,:stats,:misc,:plugin)'.format(self.table_name), heartbeat)
|
||||||
conn.commit()
|
conn.commit()
|
||||||
conn.close()
|
conn.close()
|
||||||
except sqlite3.Error:
|
except sqlite3.Error:
|
||||||
log.error(traceback.format_exc())
|
log.error(traceback.format_exc())
|
||||||
|
|
||||||
|
|
||||||
def pop(self):
|
def pop(self):
|
||||||
if not HAS_SQL:
|
if not HAS_SQL: # pragma: nocover
|
||||||
return None
|
return None
|
||||||
tries = 3
|
tries = 3
|
||||||
wait = 0.1
|
wait = 0.1
|
||||||
@ -82,42 +87,43 @@ class Queue(object):
|
|||||||
while loop and tries > -1:
|
while loop and tries > -1:
|
||||||
try:
|
try:
|
||||||
c.execute('BEGIN IMMEDIATE')
|
c.execute('BEGIN IMMEDIATE')
|
||||||
c.execute('SELECT * FROM heartbeat LIMIT 1')
|
c.execute('SELECT * FROM {0} LIMIT 1'.format(self.table_name))
|
||||||
row = c.fetchone()
|
row = c.fetchone()
|
||||||
if row is not None:
|
if row is not None:
|
||||||
values = []
|
values = []
|
||||||
clauses = []
|
clauses = []
|
||||||
index = 0
|
index = 0
|
||||||
for row_name in ['file', 'time', 'project', 'branch', 'is_write']:
|
for row_name in ['entity', 'type', 'time', 'project', 'branch', 'is_write']:
|
||||||
if row[index] is not None:
|
if row[index] is not None:
|
||||||
clauses.append('{0}=?'.format(row_name))
|
clauses.append('{0}=?'.format(row_name))
|
||||||
values.append(row[index])
|
values.append(row[index])
|
||||||
else:
|
else: # pragma: nocover
|
||||||
clauses.append('{0} IS NULL'.format(row_name))
|
clauses.append('{0} IS NULL'.format(row_name))
|
||||||
index += 1
|
index += 1
|
||||||
if len(values) > 0:
|
if len(values) > 0:
|
||||||
c.execute('DELETE FROM heartbeat WHERE {0}'.format(' AND '.join(clauses)), values)
|
c.execute('DELETE FROM {0} WHERE {1}'.format(self.table_name, ' AND '.join(clauses)), values)
|
||||||
else:
|
else: # pragma: nocover
|
||||||
c.execute('DELETE FROM heartbeat WHERE {0}'.format(' AND '.join(clauses)))
|
c.execute('DELETE FROM {0} WHERE {1}'.format(self.table_name, ' AND '.join(clauses)))
|
||||||
conn.commit()
|
conn.commit()
|
||||||
if row is not None:
|
if row is not None:
|
||||||
heartbeat = {
|
heartbeat = {
|
||||||
'file': row[0],
|
'entity': row[0],
|
||||||
'time': row[1],
|
'type': row[1],
|
||||||
'project': row[2],
|
'time': row[2],
|
||||||
'branch': row[3],
|
'project': row[3],
|
||||||
'is_write': True if row[4] is 1 else False,
|
'branch': row[4],
|
||||||
'stats': row[5],
|
'is_write': True if row[5] is 1 else False,
|
||||||
'misc': row[6],
|
'stats': row[6],
|
||||||
'plugin': row[7],
|
'misc': row[7],
|
||||||
|
'plugin': row[8],
|
||||||
}
|
}
|
||||||
loop = False
|
loop = False
|
||||||
except sqlite3.Error:
|
except sqlite3.Error: # pragma: nocover
|
||||||
log.debug(traceback.format_exc())
|
log.debug(traceback.format_exc())
|
||||||
sleep(wait)
|
sleep(wait)
|
||||||
tries -= 1
|
tries -= 1
|
||||||
try:
|
try:
|
||||||
conn.close()
|
conn.close()
|
||||||
except sqlite3.Error:
|
except sqlite3.Error: # pragma: nocover
|
||||||
log.debug(traceback.format_exc())
|
log.debug(traceback.format_exc())
|
||||||
return heartbeat
|
return heartbeat
|
@ -61,7 +61,12 @@ considered public as object names -- the API of the formatter objects is
|
|||||||
still considered an implementation detail.)
|
still considered an implementation detail.)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__version__ = '1.2.1'
|
__version__ = '1.3.0' # we use our own version number independant of the
|
||||||
|
# one in stdlib and we release this on pypi.
|
||||||
|
|
||||||
|
__external_lib__ = True # to make sure the tests really test THIS lib,
|
||||||
|
# not the builtin one in Python stdlib
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
'ArgumentParser',
|
'ArgumentParser',
|
||||||
'ArgumentError',
|
'ArgumentError',
|
||||||
@ -1045,9 +1050,13 @@ class _SubParsersAction(Action):
|
|||||||
|
|
||||||
class _ChoicesPseudoAction(Action):
|
class _ChoicesPseudoAction(Action):
|
||||||
|
|
||||||
def __init__(self, name, help):
|
def __init__(self, name, aliases, help):
|
||||||
|
metavar = dest = name
|
||||||
|
if aliases:
|
||||||
|
metavar += ' (%s)' % ', '.join(aliases)
|
||||||
sup = super(_SubParsersAction._ChoicesPseudoAction, self)
|
sup = super(_SubParsersAction._ChoicesPseudoAction, self)
|
||||||
sup.__init__(option_strings=[], dest=name, help=help)
|
sup.__init__(option_strings=[], dest=dest, help=help,
|
||||||
|
metavar=metavar)
|
||||||
|
|
||||||
def __init__(self,
|
def __init__(self,
|
||||||
option_strings,
|
option_strings,
|
||||||
@ -1075,15 +1084,22 @@ class _SubParsersAction(Action):
|
|||||||
if kwargs.get('prog') is None:
|
if kwargs.get('prog') is None:
|
||||||
kwargs['prog'] = '%s %s' % (self._prog_prefix, name)
|
kwargs['prog'] = '%s %s' % (self._prog_prefix, name)
|
||||||
|
|
||||||
|
aliases = kwargs.pop('aliases', ())
|
||||||
|
|
||||||
# create a pseudo-action to hold the choice help
|
# create a pseudo-action to hold the choice help
|
||||||
if 'help' in kwargs:
|
if 'help' in kwargs:
|
||||||
help = kwargs.pop('help')
|
help = kwargs.pop('help')
|
||||||
choice_action = self._ChoicesPseudoAction(name, help)
|
choice_action = self._ChoicesPseudoAction(name, aliases, help)
|
||||||
self._choices_actions.append(choice_action)
|
self._choices_actions.append(choice_action)
|
||||||
|
|
||||||
# create the parser and add it to the map
|
# create the parser and add it to the map
|
||||||
parser = self._parser_class(**kwargs)
|
parser = self._parser_class(**kwargs)
|
||||||
self._name_parser_map[name] = parser
|
self._name_parser_map[name] = parser
|
||||||
|
|
||||||
|
# make parser available under aliases also
|
||||||
|
for alias in aliases:
|
||||||
|
self._name_parser_map[alias] = parser
|
||||||
|
|
||||||
return parser
|
return parser
|
||||||
|
|
||||||
def _get_subactions(self):
|
def _get_subactions(self):
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user