mirror of
https://github.com/wakatime/sublime-wakatime.git
synced 2023-08-10 21:13:02 +03:00
Compare commits
25 Commits
Author | SHA1 | Date | |
---|---|---|---|
f0532f5b8e | |||
8094db9680 | |||
bf20551849 | |||
2b6e32b578 | |||
363c3d38e2 | |||
88466d7db2 | |||
122fcbbee5 | |||
c41fcec5d8 | |||
be09b34d44 | |||
e1ee1c1216 | |||
a37061924b | |||
da01fa268b | |||
c279418651 | |||
5cf2c8f7ac | |||
d1455e77a8 | |||
8499e7bafe | |||
abc26a0864 | |||
71ad97ffe9 | |||
3ec5995c99 | |||
195cf4de36 | |||
b39eefb4f5 | |||
bbf5761e26 | |||
c4df1dc633 | |||
360a491cda | |||
f61a34eda7 |
84
HISTORY.rst
84
HISTORY.rst
@ -3,6 +3,86 @@ History
|
|||||||
-------
|
-------
|
||||||
|
|
||||||
|
|
||||||
|
8.3.4 (2019-03-30)
|
||||||
|
++++++++++++++++++
|
||||||
|
|
||||||
|
- Upgrade wakatime-cli to v10.8.2.
|
||||||
|
- Detect go.mod files as Go language.
|
||||||
|
`jetbrains-wakatime#119 <https://github.com/wakatime/jetbrains-wakatime/issues/119>`_
|
||||||
|
- Detect C++ language from all C++ file extensions.
|
||||||
|
`vscode-wakatime#87 <https://github.com/wakatime/vscode-wakatime/issues/87>`_
|
||||||
|
- Add ssl_certs_file arg and config for custom ca bundles.
|
||||||
|
`#164 <https://github.com/wakatime/wakatime/issues/164>`_
|
||||||
|
- Fix bug causing random project names when hide project names enabled.
|
||||||
|
`vscode-wakatime#162 <https://github.com/wakatime/vscode-wakatime/issues/61>`_
|
||||||
|
- Add support for UNC network shares without drive letter mapped on Winows.
|
||||||
|
`#162 <https://github.com/wakatime/wakatime/issues/162>`_
|
||||||
|
|
||||||
|
|
||||||
|
8.3.3 (2018-12-19)
|
||||||
|
++++++++++++++++++
|
||||||
|
|
||||||
|
- Upgrade wakatime-cli to v10.6.1.
|
||||||
|
- Correctly parse include_only_with_project_file when set to false.
|
||||||
|
`wakatime#161 <https://github.com/wakatime/wakatime/issues/161>`_
|
||||||
|
- Support language argument for non-file entity types.
|
||||||
|
- Send 25 heartbeats per API request.
|
||||||
|
- New category "Writing Tests".
|
||||||
|
`wakatime#156 <https://github.com/wakatime/wakatime/issues/156>`_
|
||||||
|
- Fix bug caused by git config section without any submodule option defined.
|
||||||
|
`wakatime#152 <https://github.com/wakatime/wakatime/issues/152>`_
|
||||||
|
|
||||||
|
|
||||||
|
8.3.2 (2018-10-06)
|
||||||
|
++++++++++++++++++
|
||||||
|
|
||||||
|
- Send buffered heartbeats to API every 30 seconds.
|
||||||
|
|
||||||
|
|
||||||
|
8.3.1 (2018-10-05)
|
||||||
|
++++++++++++++++++
|
||||||
|
|
||||||
|
- Upgrade wakatime-cli to v10.4.1.
|
||||||
|
- Send 50 offline heartbeats to API per request with 1 second delay in between.
|
||||||
|
|
||||||
|
|
||||||
|
8.3.0 (2018-10-03)
|
||||||
|
++++++++++++++++++
|
||||||
|
|
||||||
|
- Upgrade wakatime-cli to v10.4.0.
|
||||||
|
- Support logging coding activity to remote network drive files on Windows
|
||||||
|
platform by detecting UNC path from drive letter.
|
||||||
|
`wakatime#72 <https://github.com/wakatime/wakatime/issues/72>`_
|
||||||
|
|
||||||
|
|
||||||
|
8.2.0 (2018-09-30)
|
||||||
|
++++++++++++++++++
|
||||||
|
|
||||||
|
- Prevent opening cmd window on Windows when running wakatime-cli.
|
||||||
|
`#91 <https://github.com/wakatime/sublime-wakatime/issues/91>`_
|
||||||
|
- Upgrade wakatime-cli to v10.3.0.
|
||||||
|
- Re-enable detecting projects from Subversion folder on Windows platform.
|
||||||
|
- Prevent opening cmd window on Windows when detecting project from Subversion.
|
||||||
|
- Run tests on Windows using Appveyor.
|
||||||
|
|
||||||
|
|
||||||
|
8.1.2 (2018-09-20)
|
||||||
|
++++++++++++++++++
|
||||||
|
|
||||||
|
- Upgrade wakatime-cli to v10.2.4.
|
||||||
|
- Default --sync-offline-activity to 100 instead of 5, so offline coding is
|
||||||
|
synced to dashboard faster.
|
||||||
|
- Batch heartbeats in groups of 10 per api request.
|
||||||
|
- New config hide_project_name and argument --hide-project-names for
|
||||||
|
obfuscating project names when sending coding activity to api.
|
||||||
|
- Fix mispelled Gosu language.
|
||||||
|
`wakatime#137 <https://github.com/wakatime/wakatime/issues/137>`_
|
||||||
|
- Remove metadata when hiding project or file names.
|
||||||
|
- New --local-file argument to be used when --entity is a remote file.
|
||||||
|
- New argument --sync-offline-activity for configuring the maximum offline
|
||||||
|
heartbeats to sync to the WakaTime API.
|
||||||
|
|
||||||
|
|
||||||
8.1.1 (2018-04-26)
|
8.1.1 (2018-04-26)
|
||||||
++++++++++++++++++
|
++++++++++++++++++
|
||||||
|
|
||||||
@ -33,10 +113,10 @@ History
|
|||||||
- Upgrade wakatime-cli to v10.1.2.
|
- Upgrade wakatime-cli to v10.1.2.
|
||||||
- Detect dependencies from Swift, Objective-C, TypeScript and JavaScript files.
|
- Detect dependencies from Swift, Objective-C, TypeScript and JavaScript files.
|
||||||
- Categorize .mjs files as JavaScript.
|
- Categorize .mjs files as JavaScript.
|
||||||
`#wakatime121 <https://github.com/wakatime/wakatime/issues/121>`_
|
`wakatime#121 <https://github.com/wakatime/wakatime/issues/121>`_
|
||||||
- Detect dependencies from Elm, Haskell, Haxe, Kotlin, Rust, and Scala files.
|
- Detect dependencies from Elm, Haskell, Haxe, Kotlin, Rust, and Scala files.
|
||||||
- Improved Matlab vs Objective-C language detection.
|
- Improved Matlab vs Objective-C language detection.
|
||||||
`#wakatime129 <https://github.com/wakatime/wakatime/issues/129>`_
|
`wakatime#129 <https://github.com/wakatime/wakatime/issues/129>`_
|
||||||
|
|
||||||
|
|
||||||
8.0.6 (2018-01-04)
|
8.0.6 (2018-01-04)
|
||||||
|
3
LICENSE
3
LICENSE
@ -1,7 +1,6 @@
|
|||||||
BSD 3-Clause License
|
BSD 3-Clause License
|
||||||
|
|
||||||
Copyright (c) 2014 by the respective authors (see AUTHORS file).
|
Copyright (c) 2014 Alan Hamlett.
|
||||||
All rights reserved.
|
|
||||||
|
|
||||||
Redistribution and use in source and binary forms, with or without
|
Redistribution and use in source and binary forms, with or without
|
||||||
modification, are permitted provided that the following conditions are met:
|
modification, are permitted provided that the following conditions are met:
|
||||||
|
48
WakaTime.py
48
WakaTime.py
@ -7,7 +7,7 @@ Website: https://wakatime.com/
|
|||||||
==========================================================="""
|
==========================================================="""
|
||||||
|
|
||||||
|
|
||||||
__version__ = '8.1.1'
|
__version__ = '8.3.4'
|
||||||
|
|
||||||
|
|
||||||
import sublime
|
import sublime
|
||||||
@ -18,6 +18,7 @@ import json
|
|||||||
import os
|
import os
|
||||||
import platform
|
import platform
|
||||||
import re
|
import re
|
||||||
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
import threading
|
import threading
|
||||||
@ -25,7 +26,7 @@ import traceback
|
|||||||
import urllib
|
import urllib
|
||||||
import webbrowser
|
import webbrowser
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from subprocess import Popen, STDOUT, PIPE
|
from subprocess import STDOUT, PIPE
|
||||||
from zipfile import ZipFile
|
from zipfile import ZipFile
|
||||||
try:
|
try:
|
||||||
import _winreg as winreg # py2
|
import _winreg as winreg # py2
|
||||||
@ -42,6 +43,8 @@ except ImportError:
|
|||||||
|
|
||||||
is_py2 = (sys.version_info[0] == 2)
|
is_py2 = (sys.version_info[0] == 2)
|
||||||
is_py3 = (sys.version_info[0] == 3)
|
is_py3 = (sys.version_info[0] == 3)
|
||||||
|
is_win = platform.system() == 'Windows'
|
||||||
|
|
||||||
|
|
||||||
if is_py2:
|
if is_py2:
|
||||||
def u(text):
|
def u(text):
|
||||||
@ -91,8 +94,22 @@ else:
|
|||||||
))
|
))
|
||||||
|
|
||||||
|
|
||||||
|
class Popen(subprocess.Popen):
|
||||||
|
"""Patched Popen to prevent opening cmd window on Windows platform."""
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
startupinfo = kwargs.get('startupinfo')
|
||||||
|
if is_win or True:
|
||||||
|
try:
|
||||||
|
startupinfo = startupinfo or subprocess.STARTUPINFO()
|
||||||
|
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
kwargs['startupinfo'] = startupinfo
|
||||||
|
super(Popen, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
# globals
|
# globals
|
||||||
HEARTBEAT_FREQUENCY = 2
|
|
||||||
ST_VERSION = int(sublime.version())
|
ST_VERSION = int(sublime.version())
|
||||||
PLUGIN_DIR = os.path.dirname(os.path.realpath(__file__))
|
PLUGIN_DIR = os.path.dirname(os.path.realpath(__file__))
|
||||||
API_CLIENT = os.path.join(PLUGIN_DIR, 'packages', 'wakatime', 'cli.py')
|
API_CLIENT = os.path.join(PLUGIN_DIR, 'packages', 'wakatime', 'cli.py')
|
||||||
@ -103,8 +120,11 @@ LAST_HEARTBEAT = {
|
|||||||
'file': None,
|
'file': None,
|
||||||
'is_write': False,
|
'is_write': False,
|
||||||
}
|
}
|
||||||
|
LAST_HEARTBEAT_SENT_AT = 0
|
||||||
PYTHON_LOCATION = None
|
PYTHON_LOCATION = None
|
||||||
HEARTBEATS = queue.Queue()
|
HEARTBEATS = queue.Queue()
|
||||||
|
HEARTBEAT_FREQUENCY = 2 # minutes between logging heartbeat when editing same file
|
||||||
|
SEND_BUFFER_SECONDS = 30 # seconds between sending buffered heartbeats to API
|
||||||
|
|
||||||
|
|
||||||
# Log Levels
|
# Log Levels
|
||||||
@ -433,11 +453,18 @@ def append_heartbeat(entity, timestamp, is_write, view, project, folders):
|
|||||||
}
|
}
|
||||||
|
|
||||||
# process the queue of heartbeats in the future
|
# process the queue of heartbeats in the future
|
||||||
seconds = 4
|
set_timeout(lambda: process_queue(timestamp), SEND_BUFFER_SECONDS)
|
||||||
set_timeout(process_queue, seconds)
|
|
||||||
|
|
||||||
|
|
||||||
def process_queue():
|
def process_queue(timestamp):
|
||||||
|
global LAST_HEARTBEAT_SENT_AT
|
||||||
|
|
||||||
|
# Prevent sending heartbeats more often than SEND_BUFFER_SECONDS
|
||||||
|
now = int(time.time())
|
||||||
|
if timestamp != LAST_HEARTBEAT['time'] and LAST_HEARTBEAT_SENT_AT > now - SEND_BUFFER_SECONDS:
|
||||||
|
return
|
||||||
|
LAST_HEARTBEAT_SENT_AT = now
|
||||||
|
|
||||||
try:
|
try:
|
||||||
heartbeat = HEARTBEATS.get_nowait()
|
heartbeat = HEARTBEATS.get_nowait()
|
||||||
except queue.Empty:
|
except queue.Empty:
|
||||||
@ -542,19 +569,16 @@ class SendHeartbeatsThread(threading.Thread):
|
|||||||
if self.has_extra_heartbeats:
|
if self.has_extra_heartbeats:
|
||||||
cmd.append('--extra-heartbeats')
|
cmd.append('--extra-heartbeats')
|
||||||
stdin = PIPE
|
stdin = PIPE
|
||||||
extra_heartbeats = [self.build_heartbeat(**x) for x in self.extra_heartbeats]
|
extra_heartbeats = json.dumps([self.build_heartbeat(**x) for x in self.extra_heartbeats])
|
||||||
extra_heartbeats = json.dumps(extra_heartbeats)
|
inp = "{0}\n".format(extra_heartbeats).encode('utf-8')
|
||||||
else:
|
else:
|
||||||
extra_heartbeats = None
|
extra_heartbeats = None
|
||||||
stdin = None
|
stdin = None
|
||||||
|
inp = None
|
||||||
|
|
||||||
log(DEBUG, ' '.join(obfuscate_apikey(cmd)))
|
log(DEBUG, ' '.join(obfuscate_apikey(cmd)))
|
||||||
try:
|
try:
|
||||||
process = Popen(cmd, stdin=stdin, stdout=PIPE, stderr=STDOUT)
|
process = Popen(cmd, stdin=stdin, stdout=PIPE, stderr=STDOUT)
|
||||||
inp = None
|
|
||||||
if self.has_extra_heartbeats:
|
|
||||||
inp = "{0}\n".format(extra_heartbeats)
|
|
||||||
inp = inp.encode('utf-8')
|
|
||||||
output, err = process.communicate(input=inp)
|
output, err = process.communicate(input=inp)
|
||||||
output = u(output)
|
output = u(output)
|
||||||
retcode = process.poll()
|
retcode = process.poll()
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
__title__ = 'wakatime'
|
__title__ = 'wakatime'
|
||||||
__description__ = 'Common interface to the WakaTime api.'
|
__description__ = 'Common interface to the WakaTime api.'
|
||||||
__url__ = 'https://github.com/wakatime/wakatime'
|
__url__ = 'https://github.com/wakatime/wakatime'
|
||||||
__version_info__ = ('10', '2', '1')
|
__version_info__ = ('10', '8', '2')
|
||||||
__version__ = '.'.join(__version_info__)
|
__version__ = '.'.join(__version_info__)
|
||||||
__author__ = 'Alan Hamlett'
|
__author__ = 'Alan Hamlett'
|
||||||
__author_email__ = 'alan@wakatime.com'
|
__author_email__ = 'alan@wakatime.com'
|
||||||
|
@ -99,12 +99,16 @@ def send_heartbeats(heartbeats, args, configs, use_ntlm_proxy=False):
|
|||||||
should_try_ntlm = '\\' in args.proxy
|
should_try_ntlm = '\\' in args.proxy
|
||||||
proxies['https'] = args.proxy
|
proxies['https'] = args.proxy
|
||||||
|
|
||||||
|
ssl_verify = not args.nosslverify
|
||||||
|
if args.ssl_certs_file and ssl_verify:
|
||||||
|
ssl_verify = args.ssl_certs_file
|
||||||
|
|
||||||
# send request to api
|
# send request to api
|
||||||
response, code = None, None
|
response, code = None, None
|
||||||
try:
|
try:
|
||||||
response = session.post(api_url, data=request_body, headers=headers,
|
response = session.post(api_url, data=request_body, headers=headers,
|
||||||
proxies=proxies, timeout=timeout,
|
proxies=proxies, timeout=timeout,
|
||||||
verify=not args.nosslverify)
|
verify=ssl_verify)
|
||||||
except RequestException:
|
except RequestException:
|
||||||
if should_try_ntlm:
|
if should_try_ntlm:
|
||||||
return send_heartbeats(heartbeats, args, configs, use_ntlm_proxy=True)
|
return send_heartbeats(heartbeats, args, configs, use_ntlm_proxy=True)
|
||||||
|
@ -20,7 +20,7 @@ import traceback
|
|||||||
from .__about__ import __version__
|
from .__about__ import __version__
|
||||||
from .compat import basestring
|
from .compat import basestring
|
||||||
from .configs import parseConfigFile
|
from .configs import parseConfigFile
|
||||||
from .constants import AUTH_ERROR
|
from .constants import AUTH_ERROR, DEFAULT_SYNC_OFFLINE_ACTIVITY
|
||||||
from .packages import argparse
|
from .packages import argparse
|
||||||
|
|
||||||
|
|
||||||
@ -89,8 +89,8 @@ def parse_arguments():
|
|||||||
help='Category of this heartbeat activity. Can be ' +
|
help='Category of this heartbeat activity. Can be ' +
|
||||||
'"coding", "building", "indexing", ' +
|
'"coding", "building", "indexing", ' +
|
||||||
'"debugging", "running tests", ' +
|
'"debugging", "running tests", ' +
|
||||||
'"manual testing", "browsing", ' +
|
'"writing tests", "manual testing", ' +
|
||||||
'"code reviewing" or "designing". ' +
|
'"code reviewing", "browsing", or "designing". ' +
|
||||||
'Defaults to "coding".')
|
'Defaults to "coding".')
|
||||||
parser.add_argument('--proxy', dest='proxy', action=StoreWithoutQuotes,
|
parser.add_argument('--proxy', dest='proxy', action=StoreWithoutQuotes,
|
||||||
help='Optional proxy configuration. Supports HTTPS '+
|
help='Optional proxy configuration. Supports HTTPS '+
|
||||||
@ -103,6 +103,10 @@ def parse_arguments():
|
|||||||
help='Disables SSL certificate verification for HTTPS '+
|
help='Disables SSL certificate verification for HTTPS '+
|
||||||
'requests. By default, SSL certificates are ' +
|
'requests. By default, SSL certificates are ' +
|
||||||
'verified.')
|
'verified.')
|
||||||
|
parser.add_argument('--ssl-certs-file', dest='ssl_certs_file',
|
||||||
|
action=StoreWithoutQuotes,
|
||||||
|
help='Override the bundled Python Requests CA certs ' +
|
||||||
|
'file. By default, uses certifi for ca certs.')
|
||||||
parser.add_argument('--project', dest='project', action=StoreWithoutQuotes,
|
parser.add_argument('--project', dest='project', action=StoreWithoutQuotes,
|
||||||
help='Optional project name.')
|
help='Optional project name.')
|
||||||
parser.add_argument('--alternate-project', dest='alternate_project',
|
parser.add_argument('--alternate-project', dest='alternate_project',
|
||||||
@ -116,6 +120,12 @@ def parse_arguments():
|
|||||||
action=StoreWithoutQuotes,
|
action=StoreWithoutQuotes,
|
||||||
help='Optional language name. If valid, takes ' +
|
help='Optional language name. If valid, takes ' +
|
||||||
'priority over auto-detected language.')
|
'priority over auto-detected language.')
|
||||||
|
parser.add_argument('--local-file', dest='local_file', metavar='FILE',
|
||||||
|
action=FileAction,
|
||||||
|
help='Absolute path to local file for the ' +
|
||||||
|
'heartbeat. When --entity is a remote file, ' +
|
||||||
|
'this local file will be used for stats and ' +
|
||||||
|
'just the value of --entity sent with heartbeat.')
|
||||||
parser.add_argument('--hostname', dest='hostname',
|
parser.add_argument('--hostname', dest='hostname',
|
||||||
action=StoreWithoutQuotes,
|
action=StoreWithoutQuotes,
|
||||||
help='Hostname of current machine.')
|
help='Hostname of current machine.')
|
||||||
@ -126,13 +136,23 @@ def parse_arguments():
|
|||||||
parser.add_argument('--disableoffline', dest='offline_deprecated',
|
parser.add_argument('--disableoffline', dest='offline_deprecated',
|
||||||
action='store_true',
|
action='store_true',
|
||||||
help=argparse.SUPPRESS)
|
help=argparse.SUPPRESS)
|
||||||
parser.add_argument('--hide-filenames', dest='hide_filenames',
|
parser.add_argument('--hide-file-names', dest='hide_file_names',
|
||||||
action='store_true',
|
action='store_true',
|
||||||
help='Obfuscate filenames. Will not send file names ' +
|
help='Obfuscate filenames. Will not send file names ' +
|
||||||
'to api.')
|
'to api.')
|
||||||
|
parser.add_argument('--hide-filenames', dest='hide_filenames',
|
||||||
|
action='store_true',
|
||||||
|
help=argparse.SUPPRESS)
|
||||||
parser.add_argument('--hidefilenames', dest='hidefilenames',
|
parser.add_argument('--hidefilenames', dest='hidefilenames',
|
||||||
action='store_true',
|
action='store_true',
|
||||||
help=argparse.SUPPRESS)
|
help=argparse.SUPPRESS)
|
||||||
|
parser.add_argument('--hide-project-names', dest='hide_project_names',
|
||||||
|
action='store_true',
|
||||||
|
help='Obfuscate project names. When a project ' +
|
||||||
|
'folder is detected instead of using the ' +
|
||||||
|
'folder name as the project, a ' +
|
||||||
|
'.wakatime-project file is created with a ' +
|
||||||
|
'random project name.')
|
||||||
parser.add_argument('--exclude', dest='exclude', action='append',
|
parser.add_argument('--exclude', dest='exclude', action='append',
|
||||||
help='Filename patterns to exclude from logging. ' +
|
help='Filename patterns to exclude from logging. ' +
|
||||||
'POSIX regex syntax. Can be used more than once.')
|
'POSIX regex syntax. Can be used more than once.')
|
||||||
@ -170,6 +190,17 @@ def parse_arguments():
|
|||||||
action=StoreWithoutQuotes,
|
action=StoreWithoutQuotes,
|
||||||
help='Number of seconds to wait when sending ' +
|
help='Number of seconds to wait when sending ' +
|
||||||
'heartbeats to api. Defaults to 60 seconds.')
|
'heartbeats to api. Defaults to 60 seconds.')
|
||||||
|
parser.add_argument('--sync-offline-activity',
|
||||||
|
dest='sync_offline_activity',
|
||||||
|
action=StoreWithoutQuotes,
|
||||||
|
help='Amount of offline activity to sync from your ' +
|
||||||
|
'local ~/.wakatime.db sqlite3 file to your ' +
|
||||||
|
'WakaTime Dashboard before exiting. Can be ' +
|
||||||
|
'"none" or a positive integer number. Defaults ' +
|
||||||
|
'to 5, meaning for every heartbeat sent while ' +
|
||||||
|
'online 5 offline heartbeats are synced. Can ' +
|
||||||
|
'be used without --entity to only sync offline ' +
|
||||||
|
'activity without generating new heartbeats.')
|
||||||
parser.add_argument('--config', dest='config', action=StoreWithoutQuotes,
|
parser.add_argument('--config', dest='config', action=StoreWithoutQuotes,
|
||||||
help='Defaults to ~/.wakatime.cfg.')
|
help='Defaults to ~/.wakatime.cfg.')
|
||||||
parser.add_argument('--verbose', dest='verbose', action='store_true',
|
parser.add_argument('--verbose', dest='verbose', action='store_true',
|
||||||
@ -214,9 +245,20 @@ def parse_arguments():
|
|||||||
if not args.entity:
|
if not args.entity:
|
||||||
if args.file:
|
if args.file:
|
||||||
args.entity = args.file
|
args.entity = args.file
|
||||||
else:
|
elif not args.sync_offline_activity or args.sync_offline_activity == 'none':
|
||||||
parser.error('argument --entity is required')
|
parser.error('argument --entity is required')
|
||||||
|
|
||||||
|
if not args.sync_offline_activity:
|
||||||
|
args.sync_offline_activity = DEFAULT_SYNC_OFFLINE_ACTIVITY
|
||||||
|
if args.sync_offline_activity == 'none':
|
||||||
|
args.sync_offline_activity = 0
|
||||||
|
try:
|
||||||
|
args.sync_offline_activity = int(args.sync_offline_activity)
|
||||||
|
if args.sync_offline_activity < 0:
|
||||||
|
raise Exception('Error')
|
||||||
|
except:
|
||||||
|
parser.error('argument --sync-offline-activity must be "none" or an integer number')
|
||||||
|
|
||||||
if not args.language and args.alternate_language:
|
if not args.language and args.alternate_language:
|
||||||
args.language = args.alternate_language
|
args.language = args.alternate_language
|
||||||
|
|
||||||
@ -237,7 +279,7 @@ def parse_arguments():
|
|||||||
except TypeError: # pragma: nocover
|
except TypeError: # pragma: nocover
|
||||||
pass
|
pass
|
||||||
if not args.include_only_with_project_file and configs.has_option('settings', 'include_only_with_project_file'):
|
if not args.include_only_with_project_file and configs.has_option('settings', 'include_only_with_project_file'):
|
||||||
args.include_only_with_project_file = configs.get('settings', 'include_only_with_project_file')
|
args.include_only_with_project_file = configs.get('settings', 'include_only_with_project_file') == 'true'
|
||||||
if not args.include:
|
if not args.include:
|
||||||
args.include = []
|
args.include = []
|
||||||
if configs.has_option('settings', 'include'):
|
if configs.has_option('settings', 'include'):
|
||||||
@ -249,24 +291,8 @@ def parse_arguments():
|
|||||||
pass
|
pass
|
||||||
if not args.exclude_unknown_project and configs.has_option('settings', 'exclude_unknown_project'):
|
if not args.exclude_unknown_project and configs.has_option('settings', 'exclude_unknown_project'):
|
||||||
args.exclude_unknown_project = configs.getboolean('settings', 'exclude_unknown_project')
|
args.exclude_unknown_project = configs.getboolean('settings', 'exclude_unknown_project')
|
||||||
if not args.hide_filenames and args.hidefilenames:
|
boolean_or_list('hide_file_names', args, configs, alternative_names=['hide_filenames', 'hidefilenames'])
|
||||||
args.hide_filenames = args.hidefilenames
|
boolean_or_list('hide_project_names', args, configs, alternative_names=['hide_projectnames', 'hideprojectnames'])
|
||||||
if args.hide_filenames:
|
|
||||||
args.hide_filenames = ['.*']
|
|
||||||
else:
|
|
||||||
args.hide_filenames = []
|
|
||||||
option = None
|
|
||||||
if configs.has_option('settings', 'hidefilenames'):
|
|
||||||
option = configs.get('settings', 'hidefilenames')
|
|
||||||
if configs.has_option('settings', 'hide_filenames'):
|
|
||||||
option = configs.get('settings', 'hide_filenames')
|
|
||||||
if option is not None:
|
|
||||||
if option.strip().lower() == 'true':
|
|
||||||
args.hide_filenames = ['.*']
|
|
||||||
elif option.strip().lower() != 'false':
|
|
||||||
for pattern in option.split("\n"):
|
|
||||||
if pattern.strip() != '':
|
|
||||||
args.hide_filenames.append(pattern)
|
|
||||||
if args.offline_deprecated:
|
if args.offline_deprecated:
|
||||||
args.offline = False
|
args.offline = False
|
||||||
if args.offline and configs.has_option('settings', 'offline'):
|
if args.offline and configs.has_option('settings', 'offline'):
|
||||||
@ -285,6 +311,8 @@ def parse_arguments():
|
|||||||
'domain\\user:pass.')
|
'domain\\user:pass.')
|
||||||
if configs.has_option('settings', 'no_ssl_verify'):
|
if configs.has_option('settings', 'no_ssl_verify'):
|
||||||
args.nosslverify = configs.getboolean('settings', 'no_ssl_verify')
|
args.nosslverify = configs.getboolean('settings', 'no_ssl_verify')
|
||||||
|
if configs.has_option('settings', 'ssl_certs_file'):
|
||||||
|
args.ssl_certs_file = configs.get('settings', 'ssl_certs_file')
|
||||||
if not args.verbose and configs.has_option('settings', 'verbose'):
|
if not args.verbose and configs.has_option('settings', 'verbose'):
|
||||||
args.verbose = configs.getboolean('settings', 'verbose')
|
args.verbose = configs.getboolean('settings', 'verbose')
|
||||||
if not args.verbose and configs.has_option('settings', 'debug'):
|
if not args.verbose and configs.has_option('settings', 'debug'):
|
||||||
@ -307,3 +335,30 @@ def parse_arguments():
|
|||||||
print(traceback.format_exc())
|
print(traceback.format_exc())
|
||||||
|
|
||||||
return args, configs
|
return args, configs
|
||||||
|
|
||||||
|
|
||||||
|
def boolean_or_list(config_name, args, configs, alternative_names=[]):
|
||||||
|
"""Get a boolean or list of regexes from args and configs."""
|
||||||
|
|
||||||
|
# when argument flag present, set to wildcard regex
|
||||||
|
for key in alternative_names + [config_name]:
|
||||||
|
if hasattr(args, key) and getattr(args, key):
|
||||||
|
setattr(args, config_name, ['.*'])
|
||||||
|
return
|
||||||
|
|
||||||
|
setattr(args, config_name, [])
|
||||||
|
|
||||||
|
option = None
|
||||||
|
alternative_names.insert(0, config_name)
|
||||||
|
for key in alternative_names:
|
||||||
|
if configs.has_option('settings', key):
|
||||||
|
option = configs.get('settings', key)
|
||||||
|
break
|
||||||
|
|
||||||
|
if option is not None:
|
||||||
|
if option.strip().lower() == 'true':
|
||||||
|
setattr(args, config_name, ['.*'])
|
||||||
|
elif option.strip().lower() != 'false':
|
||||||
|
for pattern in option.split("\n"):
|
||||||
|
if pattern.strip() != '':
|
||||||
|
getattr(args, config_name).append(pattern)
|
||||||
|
@ -11,11 +11,15 @@
|
|||||||
|
|
||||||
|
|
||||||
import codecs
|
import codecs
|
||||||
|
import os
|
||||||
|
import platform
|
||||||
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
|
||||||
is_py2 = (sys.version_info[0] == 2)
|
is_py2 = (sys.version_info[0] == 2)
|
||||||
is_py3 = (sys.version_info[0] == 3)
|
is_py3 = (sys.version_info[0] == 3)
|
||||||
|
is_win = platform.system() == 'Windows'
|
||||||
|
|
||||||
|
|
||||||
if is_py2: # pragma: nocover
|
if is_py2: # pragma: nocover
|
||||||
@ -98,3 +102,21 @@ try:
|
|||||||
from .packages import simplejson as json
|
from .packages import simplejson as json
|
||||||
except (ImportError, SyntaxError): # pragma: nocover
|
except (ImportError, SyntaxError): # pragma: nocover
|
||||||
import json
|
import json
|
||||||
|
|
||||||
|
|
||||||
|
class Popen(subprocess.Popen):
|
||||||
|
"""Patched Popen to prevent opening cmd window on Windows platform."""
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
startupinfo = kwargs.get('startupinfo')
|
||||||
|
if is_win or True:
|
||||||
|
try:
|
||||||
|
startupinfo = startupinfo or subprocess.STARTUPINFO()
|
||||||
|
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
kwargs['startupinfo'] = startupinfo
|
||||||
|
if 'env' not in kwargs:
|
||||||
|
kwargs['env'] = os.environ.copy()
|
||||||
|
kwargs['env']['LANG'] = 'en-US' if is_win else 'en_US.UTF-8'
|
||||||
|
subprocess.Popen.__init__(self, *args, **kwargs)
|
||||||
|
@ -45,3 +45,12 @@ Files larger than this in bytes will not have a line count stat for performance.
|
|||||||
Default is 2MB.
|
Default is 2MB.
|
||||||
"""
|
"""
|
||||||
MAX_FILE_SIZE_SUPPORTED = 2000000
|
MAX_FILE_SIZE_SUPPORTED = 2000000
|
||||||
|
|
||||||
|
""" Default limit of number of offline heartbeats to sync before exiting."""
|
||||||
|
DEFAULT_SYNC_OFFLINE_ACTIVITY = 100
|
||||||
|
|
||||||
|
""" Number of heartbeats per api request.
|
||||||
|
Even when sending more heartbeats, this is the number of heartbeats sent per
|
||||||
|
individual https request to the WakaTime API.
|
||||||
|
"""
|
||||||
|
HEARTBEATS_PER_REQUEST = 25
|
||||||
|
@ -131,5 +131,5 @@ class DependencyParser(object):
|
|||||||
if self.parser:
|
if self.parser:
|
||||||
plugin = self.parser(self.source_file, lexer=self.lexer)
|
plugin = self.parser(self.source_file, lexer=self.lexer)
|
||||||
dependencies = plugin.parse()
|
dependencies = plugin.parse()
|
||||||
return list(set(dependencies))
|
return list(filter(bool, set(dependencies)))
|
||||||
return []
|
return []
|
||||||
|
@ -10,8 +10,9 @@
|
|||||||
import os
|
import os
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
|
from subprocess import PIPE
|
||||||
|
|
||||||
from .compat import u, json
|
from .compat import u, json, is_win, Popen
|
||||||
from .exceptions import SkipHeartbeat
|
from .exceptions import SkipHeartbeat
|
||||||
from .project import get_project_info
|
from .project import get_project_info
|
||||||
from .stats import get_file_stats
|
from .stats import get_file_stats
|
||||||
@ -42,6 +43,8 @@ class Heartbeat(object):
|
|||||||
cursorpos = None
|
cursorpos = None
|
||||||
user_agent = None
|
user_agent = None
|
||||||
|
|
||||||
|
_sensitive = ('dependencies', 'lines', 'lineno', 'cursorpos', 'branch')
|
||||||
|
|
||||||
def __init__(self, data, args, configs, _clone=None):
|
def __init__(self, data, args, configs, _clone=None):
|
||||||
if not data:
|
if not data:
|
||||||
self.skip = u('Skipping because heartbeat data is missing.')
|
self.skip = u('Skipping because heartbeat data is missing.')
|
||||||
@ -67,6 +70,7 @@ class Heartbeat(object):
|
|||||||
'debugging',
|
'debugging',
|
||||||
'running tests',
|
'running tests',
|
||||||
'manual testing',
|
'manual testing',
|
||||||
|
'writing tests',
|
||||||
'browsing',
|
'browsing',
|
||||||
'code reviewing',
|
'code reviewing',
|
||||||
'designing',
|
'designing',
|
||||||
@ -83,13 +87,17 @@ class Heartbeat(object):
|
|||||||
return
|
return
|
||||||
if self.type == 'file':
|
if self.type == 'file':
|
||||||
self.entity = format_file_path(self.entity)
|
self.entity = format_file_path(self.entity)
|
||||||
if not self.entity or not os.path.isfile(self.entity):
|
self._format_local_file()
|
||||||
|
if not self._file_exists():
|
||||||
self.skip = u('File does not exist; ignoring this heartbeat.')
|
self.skip = u('File does not exist; ignoring this heartbeat.')
|
||||||
return
|
return
|
||||||
if self._excluded_by_missing_project_file():
|
if self._excluded_by_missing_project_file():
|
||||||
self.skip = u('Skipping because missing .wakatime-project file in parent path.')
|
self.skip = u('Skipping because missing .wakatime-project file in parent path.')
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if args.local_file and not os.path.isfile(args.local_file):
|
||||||
|
args.local_file = None
|
||||||
|
|
||||||
project, branch = get_project_info(configs, self, data)
|
project, branch = get_project_info(configs, self, data)
|
||||||
self.project = project
|
self.project = project
|
||||||
self.branch = branch
|
self.branch = branch
|
||||||
@ -104,7 +112,8 @@ class Heartbeat(object):
|
|||||||
lineno=data.get('lineno'),
|
lineno=data.get('lineno'),
|
||||||
cursorpos=data.get('cursorpos'),
|
cursorpos=data.get('cursorpos'),
|
||||||
plugin=args.plugin,
|
plugin=args.plugin,
|
||||||
language=data.get('language'))
|
language=data.get('language'),
|
||||||
|
local_file=args.local_file)
|
||||||
except SkipHeartbeat as ex:
|
except SkipHeartbeat as ex:
|
||||||
self.skip = u(ex) or 'Skipping'
|
self.skip = u(ex) or 'Skipping'
|
||||||
return
|
return
|
||||||
@ -132,7 +141,7 @@ class Heartbeat(object):
|
|||||||
Returns a Heartbeat.
|
Returns a Heartbeat.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if not self.args.hide_filenames:
|
if not self.args.hide_file_names:
|
||||||
return self
|
return self
|
||||||
|
|
||||||
if self.entity is None:
|
if self.entity is None:
|
||||||
@ -141,29 +150,12 @@ class Heartbeat(object):
|
|||||||
if self.type != 'file':
|
if self.type != 'file':
|
||||||
return self
|
return self
|
||||||
|
|
||||||
for pattern in self.args.hide_filenames:
|
if self.should_obfuscate_filename():
|
||||||
try:
|
self._sanitize_metadata()
|
||||||
compiled = re.compile(pattern, re.IGNORECASE)
|
extension = u(os.path.splitext(self.entity)[1])
|
||||||
if compiled.search(self.entity):
|
self.entity = u('HIDDEN{0}').format(extension)
|
||||||
|
elif self.should_obfuscate_project():
|
||||||
sanitized = {}
|
self._sanitize_metadata()
|
||||||
sensitive = ['dependencies', 'lines', 'lineno', 'cursorpos', 'branch']
|
|
||||||
for key, val in self.items():
|
|
||||||
if key in sensitive:
|
|
||||||
sanitized[key] = None
|
|
||||||
else:
|
|
||||||
sanitized[key] = val
|
|
||||||
|
|
||||||
extension = u(os.path.splitext(self.entity)[1])
|
|
||||||
sanitized['entity'] = u('HIDDEN{0}').format(extension)
|
|
||||||
|
|
||||||
return self.update(sanitized)
|
|
||||||
|
|
||||||
except re.error as ex:
|
|
||||||
log.warning(u('Regex error ({msg}) for include pattern: {pattern}').format(
|
|
||||||
msg=u(ex),
|
|
||||||
pattern=u(pattern),
|
|
||||||
))
|
|
||||||
|
|
||||||
return self
|
return self
|
||||||
|
|
||||||
@ -201,6 +193,38 @@ class Heartbeat(object):
|
|||||||
is_write=self.is_write,
|
is_write=self.is_write,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def should_obfuscate_filename(self):
|
||||||
|
"""Returns True if hide_file_names is true or the entity file path
|
||||||
|
matches one in the list of obfuscated file paths."""
|
||||||
|
|
||||||
|
for pattern in self.args.hide_file_names:
|
||||||
|
try:
|
||||||
|
compiled = re.compile(pattern, re.IGNORECASE)
|
||||||
|
if compiled.search(self.entity):
|
||||||
|
return True
|
||||||
|
except re.error as ex:
|
||||||
|
log.warning(u('Regex error ({msg}) for hide_file_names pattern: {pattern}').format(
|
||||||
|
msg=u(ex),
|
||||||
|
pattern=u(pattern),
|
||||||
|
))
|
||||||
|
return False
|
||||||
|
|
||||||
|
def should_obfuscate_project(self):
|
||||||
|
"""Returns True if hide_project_names is true or the entity file path
|
||||||
|
matches one in the list of obfuscated project paths."""
|
||||||
|
|
||||||
|
for pattern in self.args.hide_project_names:
|
||||||
|
try:
|
||||||
|
compiled = re.compile(pattern, re.IGNORECASE)
|
||||||
|
if compiled.search(self.entity):
|
||||||
|
return True
|
||||||
|
except re.error as ex:
|
||||||
|
log.warning(u('Regex error ({msg}) for hide_project_names pattern: {pattern}').format(
|
||||||
|
msg=u(ex),
|
||||||
|
pattern=u(pattern),
|
||||||
|
))
|
||||||
|
return False
|
||||||
|
|
||||||
def _unicode(self, value):
|
def _unicode(self, value):
|
||||||
if value is None:
|
if value is None:
|
||||||
return None
|
return None
|
||||||
@ -211,6 +235,91 @@ class Heartbeat(object):
|
|||||||
return None
|
return None
|
||||||
return [self._unicode(value) for value in values]
|
return [self._unicode(value) for value in values]
|
||||||
|
|
||||||
|
def _file_exists(self):
|
||||||
|
return (self.entity and os.path.isfile(self.entity) or
|
||||||
|
self.args.local_file and os.path.isfile(self.args.local_file))
|
||||||
|
|
||||||
|
def _format_local_file(self):
|
||||||
|
"""When args.local_file empty on Windows, tries to map args.entity to a
|
||||||
|
unc path.
|
||||||
|
|
||||||
|
Updates args.local_file in-place without returning anything.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if self.type != 'file':
|
||||||
|
return
|
||||||
|
|
||||||
|
if not self.entity:
|
||||||
|
return
|
||||||
|
|
||||||
|
if not is_win:
|
||||||
|
return
|
||||||
|
|
||||||
|
if self._file_exists():
|
||||||
|
return
|
||||||
|
|
||||||
|
self.args.local_file = self._to_unc_path(self.entity)
|
||||||
|
|
||||||
|
def _to_unc_path(self, filepath):
|
||||||
|
drive, rest = self._splitdrive(filepath)
|
||||||
|
if not drive:
|
||||||
|
return filepath
|
||||||
|
|
||||||
|
stdout = None
|
||||||
|
try:
|
||||||
|
stdout, stderr = Popen(['net', 'use'], stdout=PIPE, stderr=PIPE).communicate()
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
if stdout:
|
||||||
|
cols = None
|
||||||
|
for line in stdout.strip().splitlines()[1:]:
|
||||||
|
line = u(line)
|
||||||
|
if not line.strip():
|
||||||
|
continue
|
||||||
|
if not cols:
|
||||||
|
cols = self._unc_columns(line)
|
||||||
|
continue
|
||||||
|
start, end = cols.get('local', (0, 0))
|
||||||
|
if not start and not end:
|
||||||
|
break
|
||||||
|
local = line[start:end].strip().split(':')[0].upper()
|
||||||
|
if not local.isalpha():
|
||||||
|
continue
|
||||||
|
if local == drive:
|
||||||
|
start, end = cols.get('remote', (0, 0))
|
||||||
|
if not start and not end:
|
||||||
|
break
|
||||||
|
remote = line[start:end].strip()
|
||||||
|
return remote + rest
|
||||||
|
|
||||||
|
return filepath
|
||||||
|
|
||||||
|
def _unc_columns(self, line):
|
||||||
|
cols = {}
|
||||||
|
current_col = u('')
|
||||||
|
newcol = False
|
||||||
|
start, end = 0, 0
|
||||||
|
for char in line:
|
||||||
|
if char.isalpha():
|
||||||
|
if newcol:
|
||||||
|
cols[current_col.strip().lower()] = (start, end)
|
||||||
|
current_col = u('')
|
||||||
|
start = end
|
||||||
|
newcol = False
|
||||||
|
current_col += u(char)
|
||||||
|
else:
|
||||||
|
newcol = True
|
||||||
|
end += 1
|
||||||
|
if start != end and current_col:
|
||||||
|
cols[current_col.strip().lower()] = (start, -1)
|
||||||
|
return cols
|
||||||
|
|
||||||
|
def _splitdrive(self, filepath):
|
||||||
|
if filepath[1:2] != ':' or not filepath[0].isalpha():
|
||||||
|
return None, filepath
|
||||||
|
return filepath[0].upper(), filepath[2:]
|
||||||
|
|
||||||
def _excluded_by_pattern(self):
|
def _excluded_by_pattern(self):
|
||||||
return should_exclude(self.entity, self.args.include, self.args.exclude)
|
return should_exclude(self.entity, self.args.include, self.args.exclude)
|
||||||
|
|
||||||
@ -224,6 +333,10 @@ class Heartbeat(object):
|
|||||||
return False
|
return False
|
||||||
return find_project_file(self.entity) is None
|
return find_project_file(self.entity) is None
|
||||||
|
|
||||||
|
def _sanitize_metadata(self):
|
||||||
|
for key in self._sensitive:
|
||||||
|
setattr(self, key, None)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return self.json()
|
return self.json()
|
||||||
|
|
||||||
|
@ -27,7 +27,7 @@
|
|||||||
"f#": "F#",
|
"f#": "F#",
|
||||||
"fortran": "Fortran",
|
"fortran": "Fortran",
|
||||||
"go": "Go",
|
"go": "Go",
|
||||||
"gous": "Gosu",
|
"gosu": "Gosu",
|
||||||
"groovy": "Groovy",
|
"groovy": "Groovy",
|
||||||
"haml": "Haml",
|
"haml": "Haml",
|
||||||
"haskell": "Haskell",
|
"haskell": "Haskell",
|
||||||
|
@ -14,6 +14,7 @@ from __future__ import print_function
|
|||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
import time
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
pwd = os.path.dirname(os.path.abspath(__file__))
|
pwd = os.path.dirname(os.path.abspath(__file__))
|
||||||
@ -24,7 +25,7 @@ from .__about__ import __version__
|
|||||||
from .api import send_heartbeats
|
from .api import send_heartbeats
|
||||||
from .arguments import parse_arguments
|
from .arguments import parse_arguments
|
||||||
from .compat import u, json
|
from .compat import u, json
|
||||||
from .constants import SUCCESS, UNKNOWN_ERROR
|
from .constants import SUCCESS, UNKNOWN_ERROR, HEARTBEATS_PER_REQUEST
|
||||||
from .logger import setup_logging
|
from .logger import setup_logging
|
||||||
|
|
||||||
log = logging.getLogger('WakaTime')
|
log = logging.getLogger('WakaTime')
|
||||||
@ -63,12 +64,23 @@ def execute(argv=None):
|
|||||||
msg=u(ex),
|
msg=u(ex),
|
||||||
))
|
))
|
||||||
|
|
||||||
retval = send_heartbeats(heartbeats, args, configs)
|
retval = SUCCESS
|
||||||
|
while heartbeats:
|
||||||
|
retval = send_heartbeats(heartbeats[:HEARTBEATS_PER_REQUEST], args, configs)
|
||||||
|
heartbeats = heartbeats[HEARTBEATS_PER_REQUEST:]
|
||||||
|
if retval != SUCCESS:
|
||||||
|
break
|
||||||
|
|
||||||
|
if heartbeats:
|
||||||
|
Queue(args, configs).push_many(heartbeats)
|
||||||
|
|
||||||
if retval == SUCCESS:
|
if retval == SUCCESS:
|
||||||
queue = Queue(args, configs)
|
queue = Queue(args, configs)
|
||||||
offline_heartbeats = queue.pop_many()
|
for offline_heartbeats in queue.pop_many(args.sync_offline_activity):
|
||||||
if len(offline_heartbeats) > 0:
|
time.sleep(1)
|
||||||
retval = send_heartbeats(offline_heartbeats, args, configs)
|
retval = send_heartbeats(offline_heartbeats, args, configs)
|
||||||
|
if retval != SUCCESS:
|
||||||
|
break
|
||||||
|
|
||||||
return retval
|
return retval
|
||||||
|
|
||||||
|
@ -15,6 +15,7 @@ import os
|
|||||||
from time import sleep
|
from time import sleep
|
||||||
|
|
||||||
from .compat import json
|
from .compat import json
|
||||||
|
from .constants import DEFAULT_SYNC_OFFLINE_ACTIVITY, HEARTBEATS_PER_REQUEST
|
||||||
from .heartbeat import Heartbeat
|
from .heartbeat import Heartbeat
|
||||||
|
|
||||||
|
|
||||||
@ -104,19 +105,23 @@ class Queue(object):
|
|||||||
|
|
||||||
def pop_many(self, limit=None):
|
def pop_many(self, limit=None):
|
||||||
if limit is None:
|
if limit is None:
|
||||||
limit = 5
|
limit = DEFAULT_SYNC_OFFLINE_ACTIVITY
|
||||||
|
|
||||||
heartbeats = []
|
heartbeats = []
|
||||||
|
|
||||||
count = 0
|
count = 0
|
||||||
while limit == 0 or count < limit:
|
while count < limit:
|
||||||
heartbeat = self.pop()
|
heartbeat = self.pop()
|
||||||
if not heartbeat:
|
if not heartbeat:
|
||||||
break
|
break
|
||||||
heartbeats.append(heartbeat)
|
heartbeats.append(heartbeat)
|
||||||
count += 1
|
count += 1
|
||||||
|
if count % HEARTBEATS_PER_REQUEST == 0:
|
||||||
|
yield heartbeats
|
||||||
|
heartbeats = []
|
||||||
|
|
||||||
return heartbeats
|
if heartbeats:
|
||||||
|
yield heartbeats
|
||||||
|
|
||||||
def _get_db_file(self):
|
def _get_db_file(self):
|
||||||
home = '~'
|
home = '~'
|
||||||
|
@ -9,8 +9,11 @@
|
|||||||
:license: BSD, see LICENSE for more details.
|
:license: BSD, see LICENSE for more details.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
import logging
|
import logging
|
||||||
|
import random
|
||||||
|
|
||||||
|
from .compat import open
|
||||||
from .projects.git import Git
|
from .projects.git import Git
|
||||||
from .projects.mercurial import Mercurial
|
from .projects.mercurial import Mercurial
|
||||||
from .projects.projectfile import ProjectFile
|
from .projects.projectfile import ProjectFile
|
||||||
@ -65,6 +68,10 @@ def get_project_info(configs, heartbeat, data):
|
|||||||
if project_name is None:
|
if project_name is None:
|
||||||
project_name = data.get('project') or heartbeat.args.project
|
project_name = data.get('project') or heartbeat.args.project
|
||||||
|
|
||||||
|
hide_project = heartbeat.should_obfuscate_project()
|
||||||
|
if hide_project and project_name is not None:
|
||||||
|
return project_name, None
|
||||||
|
|
||||||
if project_name is None or branch_name is None:
|
if project_name is None or branch_name is None:
|
||||||
|
|
||||||
for plugin_cls in REV_CONTROL_PLUGINS:
|
for plugin_cls in REV_CONTROL_PLUGINS:
|
||||||
@ -76,9 +83,18 @@ def get_project_info(configs, heartbeat, data):
|
|||||||
if project.process():
|
if project.process():
|
||||||
project_name = project_name or project.name()
|
project_name = project_name or project.name()
|
||||||
branch_name = branch_name or project.branch()
|
branch_name = branch_name or project.branch()
|
||||||
|
if hide_project:
|
||||||
|
branch_name = None
|
||||||
|
project_name = generate_project_name()
|
||||||
|
project_file = os.path.join(project.folder(), '.wakatime-project')
|
||||||
|
try:
|
||||||
|
with open(project_file, 'w') as fh:
|
||||||
|
fh.write(project_name)
|
||||||
|
except IOError:
|
||||||
|
project_name = None
|
||||||
break
|
break
|
||||||
|
|
||||||
if project_name is None:
|
if project_name is None and not hide_project:
|
||||||
project_name = data.get('alternate_project') or heartbeat.args.alternate_project
|
project_name = data.get('alternate_project') or heartbeat.args.alternate_project
|
||||||
|
|
||||||
return project_name, branch_name
|
return project_name, branch_name
|
||||||
@ -88,3 +104,42 @@ def get_configs_for_plugin(plugin_name, configs):
|
|||||||
if configs and configs.has_section(plugin_name):
|
if configs and configs.has_section(plugin_name):
|
||||||
return dict(configs.items(plugin_name))
|
return dict(configs.items(plugin_name))
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def generate_project_name():
|
||||||
|
"""Generates a random project name."""
|
||||||
|
|
||||||
|
adjectives = [
|
||||||
|
'aged', 'ancient', 'autumn', 'billowing', 'bitter', 'black', 'blue', 'bold',
|
||||||
|
'broad', 'broken', 'calm', 'cold', 'cool', 'crimson', 'curly', 'damp',
|
||||||
|
'dark', 'dawn', 'delicate', 'divine', 'dry', 'empty', 'falling', 'fancy',
|
||||||
|
'flat', 'floral', 'fragrant', 'frosty', 'gentle', 'green', 'hidden', 'holy',
|
||||||
|
'icy', 'jolly', 'late', 'lingering', 'little', 'lively', 'long', 'lucky',
|
||||||
|
'misty', 'morning', 'muddy', 'mute', 'nameless', 'noisy', 'odd', 'old',
|
||||||
|
'orange', 'patient', 'plain', 'polished', 'proud', 'purple', 'quiet', 'rapid',
|
||||||
|
'raspy', 'red', 'restless', 'rough', 'round', 'royal', 'shiny', 'shrill',
|
||||||
|
'shy', 'silent', 'small', 'snowy', 'soft', 'solitary', 'sparkling', 'spring',
|
||||||
|
'square', 'steep', 'still', 'summer', 'super', 'sweet', 'throbbing', 'tight',
|
||||||
|
'tiny', 'twilight', 'wandering', 'weathered', 'white', 'wild', 'winter', 'wispy',
|
||||||
|
'withered', 'yellow', 'young'
|
||||||
|
]
|
||||||
|
nouns = [
|
||||||
|
'art', 'band', 'bar', 'base', 'bird', 'block', 'boat', 'bonus',
|
||||||
|
'bread', 'breeze', 'brook', 'bush', 'butterfly', 'cake', 'cell', 'cherry',
|
||||||
|
'cloud', 'credit', 'darkness', 'dawn', 'dew', 'disk', 'dream', 'dust',
|
||||||
|
'feather', 'field', 'fire', 'firefly', 'flower', 'fog', 'forest', 'frog',
|
||||||
|
'frost', 'glade', 'glitter', 'grass', 'hall', 'hat', 'haze', 'heart',
|
||||||
|
'hill', 'king', 'lab', 'lake', 'leaf', 'limit', 'math', 'meadow',
|
||||||
|
'mode', 'moon', 'morning', 'mountain', 'mouse', 'mud', 'night', 'paper',
|
||||||
|
'pine', 'poetry', 'pond', 'queen', 'rain', 'recipe', 'resonance', 'rice',
|
||||||
|
'river', 'salad', 'scene', 'sea', 'shadow', 'shape', 'silence', 'sky',
|
||||||
|
'smoke', 'snow', 'snowflake', 'sound', 'star', 'sun', 'sun', 'sunset',
|
||||||
|
'surf', 'term', 'thunder', 'tooth', 'tree', 'truth', 'union', 'unit',
|
||||||
|
'violet', 'voice', 'water', 'waterfall', 'wave', 'wildflower', 'wind', 'wood'
|
||||||
|
]
|
||||||
|
numbers = [str(x) for x in range(10)]
|
||||||
|
return ' '.join([
|
||||||
|
random.choice(adjectives).capitalize(),
|
||||||
|
random.choice(nouns).capitalize(),
|
||||||
|
random.choice(numbers) + random.choice(numbers),
|
||||||
|
])
|
||||||
|
@ -43,3 +43,8 @@ class BaseProject(object):
|
|||||||
""" Returns the current branch.
|
""" Returns the current branch.
|
||||||
"""
|
"""
|
||||||
raise NotYetImplemented()
|
raise NotYetImplemented()
|
||||||
|
|
||||||
|
def folder(self):
|
||||||
|
""" Returns the project's top folder path.
|
||||||
|
"""
|
||||||
|
raise NotYetImplemented()
|
||||||
|
@ -25,6 +25,7 @@ class Git(BaseProject):
|
|||||||
_submodule = None
|
_submodule = None
|
||||||
_project_name = None
|
_project_name = None
|
||||||
_head_file = None
|
_head_file = None
|
||||||
|
_project_folder = None
|
||||||
|
|
||||||
def process(self):
|
def process(self):
|
||||||
return self._find_git_config_file(self.path)
|
return self._find_git_config_file(self.path)
|
||||||
@ -40,6 +41,9 @@ class Git(BaseProject):
|
|||||||
return self._get_branch_from_head_file(line)
|
return self._get_branch_from_head_file(line)
|
||||||
return u('master')
|
return u('master')
|
||||||
|
|
||||||
|
def folder(self):
|
||||||
|
return self._project_folder
|
||||||
|
|
||||||
def _find_git_config_file(self, path):
|
def _find_git_config_file(self, path):
|
||||||
path = os.path.realpath(path)
|
path = os.path.realpath(path)
|
||||||
if os.path.isfile(path):
|
if os.path.isfile(path):
|
||||||
@ -47,6 +51,7 @@ class Git(BaseProject):
|
|||||||
if os.path.isfile(os.path.join(path, '.git', 'config')):
|
if os.path.isfile(os.path.join(path, '.git', 'config')):
|
||||||
self._project_name = os.path.basename(path)
|
self._project_name = os.path.basename(path)
|
||||||
self._head_file = os.path.join(path, '.git', 'HEAD')
|
self._head_file = os.path.join(path, '.git', 'HEAD')
|
||||||
|
self._project_folder = path
|
||||||
return True
|
return True
|
||||||
|
|
||||||
link_path = self._path_from_gitdir_link_file(path)
|
link_path = self._path_from_gitdir_link_file(path)
|
||||||
@ -56,12 +61,14 @@ class Git(BaseProject):
|
|||||||
if self._is_worktree(link_path):
|
if self._is_worktree(link_path):
|
||||||
self._project_name = self._project_from_worktree(link_path)
|
self._project_name = self._project_from_worktree(link_path)
|
||||||
self._head_file = os.path.join(link_path, 'HEAD')
|
self._head_file = os.path.join(link_path, 'HEAD')
|
||||||
|
self._project_folder = path
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# next check if this is a submodule
|
# next check if this is a submodule
|
||||||
if self._submodules_supported_for_path(path):
|
if self._submodules_supported_for_path(path):
|
||||||
self._project_name = os.path.basename(path)
|
self._project_name = os.path.basename(path)
|
||||||
self._head_file = os.path.join(link_path, 'HEAD')
|
self._head_file = os.path.join(link_path, 'HEAD')
|
||||||
|
self._project_folder = path
|
||||||
return True
|
return True
|
||||||
|
|
||||||
split_path = os.path.split(path)
|
split_path = os.path.split(path)
|
||||||
@ -80,10 +87,10 @@ class Git(BaseProject):
|
|||||||
|
|
||||||
disabled = self._configs.get('submodules_disabled')
|
disabled = self._configs.get('submodules_disabled')
|
||||||
|
|
||||||
|
if not disabled or disabled.strip().lower() == 'false':
|
||||||
|
return True
|
||||||
if disabled.strip().lower() == 'true':
|
if disabled.strip().lower() == 'true':
|
||||||
return False
|
return False
|
||||||
if disabled.strip().lower() == 'false':
|
|
||||||
return True
|
|
||||||
|
|
||||||
for pattern in disabled.split("\n"):
|
for pattern in disabled.split("\n"):
|
||||||
if pattern.strip():
|
if pattern.strip():
|
||||||
|
@ -47,6 +47,11 @@ class Mercurial(BaseProject):
|
|||||||
log.traceback(logging.WARNING)
|
log.traceback(logging.WARNING)
|
||||||
return u('default')
|
return u('default')
|
||||||
|
|
||||||
|
def folder(self):
|
||||||
|
if self.configDir:
|
||||||
|
return os.path.dirname(self.configDir)
|
||||||
|
return None
|
||||||
|
|
||||||
def _find_hg_config_dir(self, path):
|
def _find_hg_config_dir(self, path):
|
||||||
path = os.path.realpath(path)
|
path = os.path.realpath(path)
|
||||||
if os.path.isfile(path):
|
if os.path.isfile(path):
|
||||||
|
@ -12,10 +12,10 @@
|
|||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import platform
|
import platform
|
||||||
from subprocess import Popen, PIPE
|
from subprocess import PIPE
|
||||||
|
|
||||||
from .base import BaseProject
|
from .base import BaseProject
|
||||||
from ..compat import u, open
|
from ..compat import u, open, Popen
|
||||||
try:
|
try:
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
except ImportError: # pragma: nocover
|
except ImportError: # pragma: nocover
|
||||||
@ -41,6 +41,11 @@ class Subversion(BaseProject):
|
|||||||
return None # pragma: nocover
|
return None # pragma: nocover
|
||||||
return u(self.info['URL'].split('/')[-1].split('\\')[-1])
|
return u(self.info['URL'].split('/')[-1].split('\\')[-1])
|
||||||
|
|
||||||
|
def folder(self):
|
||||||
|
if 'Repository Root' not in self.info:
|
||||||
|
return None
|
||||||
|
return self.info['Repository Root']
|
||||||
|
|
||||||
def _find_binary(self):
|
def _find_binary(self):
|
||||||
if self.binary_location:
|
if self.binary_location:
|
||||||
return self.binary_location
|
return self.binary_location
|
||||||
@ -65,24 +70,22 @@ class Subversion(BaseProject):
|
|||||||
if not self._is_mac() or self._has_xcode_tools():
|
if not self._is_mac() or self._has_xcode_tools():
|
||||||
stdout = None
|
stdout = None
|
||||||
try:
|
try:
|
||||||
os.environ['LANG'] = 'en_US'
|
stdout, stderr = Popen(
|
||||||
stdout, stderr = Popen([
|
[self._find_binary(), 'info', os.path.realpath(path)],
|
||||||
self._find_binary(), 'info', os.path.realpath(path)
|
stdout=PIPE,
|
||||||
], stdout=PIPE, stderr=PIPE).communicate()
|
stderr=PIPE,
|
||||||
|
).communicate()
|
||||||
except OSError:
|
except OSError:
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
if stdout:
|
if stdout:
|
||||||
for line in stdout.splitlines():
|
for line in stdout.splitlines():
|
||||||
line = u(line)
|
line = u(line).split(': ', 1)
|
||||||
line = line.split(': ', 1)
|
|
||||||
if len(line) == 2:
|
if len(line) == 2:
|
||||||
info[line[0]] = line[1]
|
info[line[0]] = line[1]
|
||||||
return info
|
return info
|
||||||
|
|
||||||
def _find_project_base(self, path, found=False):
|
def _find_project_base(self, path, found=False):
|
||||||
if platform.system() == 'Windows':
|
|
||||||
return False # pragma: nocover
|
|
||||||
path = os.path.realpath(path)
|
path = os.path.realpath(path)
|
||||||
if os.path.isfile(path):
|
if os.path.isfile(path):
|
||||||
path = os.path.split(path)[0]
|
path = os.path.split(path)[0]
|
||||||
|
@ -25,6 +25,7 @@ from .packages.pygments.lexers import (
|
|||||||
_fn_matches,
|
_fn_matches,
|
||||||
basename,
|
basename,
|
||||||
ClassNotFound,
|
ClassNotFound,
|
||||||
|
CppLexer,
|
||||||
find_lexer_class,
|
find_lexer_class,
|
||||||
get_lexer_by_name,
|
get_lexer_by_name,
|
||||||
)
|
)
|
||||||
@ -40,36 +41,33 @@ log = logging.getLogger('WakaTime')
|
|||||||
|
|
||||||
|
|
||||||
def get_file_stats(file_name, entity_type='file', lineno=None, cursorpos=None,
|
def get_file_stats(file_name, entity_type='file', lineno=None, cursorpos=None,
|
||||||
plugin=None, language=None):
|
plugin=None, language=None, local_file=None):
|
||||||
if entity_type != 'file':
|
"""Returns a hash of information about the entity."""
|
||||||
stats = {
|
|
||||||
'language': None,
|
language = standardize_language(language, plugin)
|
||||||
'dependencies': [],
|
stats = {
|
||||||
'lines': None,
|
'language': language,
|
||||||
'lineno': lineno,
|
'dependencies': [],
|
||||||
'cursorpos': cursorpos,
|
'lines': None,
|
||||||
}
|
'lineno': lineno,
|
||||||
else:
|
'cursorpos': cursorpos,
|
||||||
language, lexer = standardize_language(language, plugin)
|
}
|
||||||
|
|
||||||
|
if entity_type == 'file':
|
||||||
|
lexer = get_lexer(language)
|
||||||
if not language:
|
if not language:
|
||||||
language, lexer = guess_language(file_name)
|
language, lexer = guess_language(file_name, local_file)
|
||||||
|
parser = DependencyParser(local_file or file_name, lexer)
|
||||||
|
stats.update({
|
||||||
|
'language': use_root_language(language, lexer),
|
||||||
|
'dependencies': parser.parse(),
|
||||||
|
'lines': number_lines_in_file(local_file or file_name),
|
||||||
|
})
|
||||||
|
|
||||||
language = use_root_language(language, lexer)
|
|
||||||
|
|
||||||
parser = DependencyParser(file_name, lexer)
|
|
||||||
dependencies = parser.parse()
|
|
||||||
|
|
||||||
stats = {
|
|
||||||
'language': language,
|
|
||||||
'dependencies': dependencies,
|
|
||||||
'lines': number_lines_in_file(file_name),
|
|
||||||
'lineno': lineno,
|
|
||||||
'cursorpos': cursorpos,
|
|
||||||
}
|
|
||||||
return stats
|
return stats
|
||||||
|
|
||||||
|
|
||||||
def guess_language(file_name):
|
def guess_language(file_name, local_file):
|
||||||
"""Guess lexer and language for a file.
|
"""Guess lexer and language for a file.
|
||||||
|
|
||||||
Returns a tuple of (language_str, lexer_obj).
|
Returns a tuple of (language_str, lexer_obj).
|
||||||
@ -81,14 +79,14 @@ def guess_language(file_name):
|
|||||||
if language:
|
if language:
|
||||||
lexer = get_lexer(language)
|
lexer = get_lexer(language)
|
||||||
else:
|
else:
|
||||||
lexer = smart_guess_lexer(file_name)
|
lexer = smart_guess_lexer(file_name, local_file)
|
||||||
if lexer:
|
if lexer:
|
||||||
language = u(lexer.name)
|
language = u(lexer.name)
|
||||||
|
|
||||||
return language, lexer
|
return language, lexer
|
||||||
|
|
||||||
|
|
||||||
def smart_guess_lexer(file_name):
|
def smart_guess_lexer(file_name, local_file):
|
||||||
"""Guess Pygments lexer for a file.
|
"""Guess Pygments lexer for a file.
|
||||||
|
|
||||||
Looks for a vim modeline in file contents, then compares the accuracy
|
Looks for a vim modeline in file contents, then compares the accuracy
|
||||||
@ -99,7 +97,7 @@ def smart_guess_lexer(file_name):
|
|||||||
|
|
||||||
text = get_file_head(file_name)
|
text = get_file_head(file_name)
|
||||||
|
|
||||||
lexer1, accuracy1 = guess_lexer_using_filename(file_name, text)
|
lexer1, accuracy1 = guess_lexer_using_filename(local_file or file_name, text)
|
||||||
lexer2, accuracy2 = guess_lexer_using_modeline(text)
|
lexer2, accuracy2 = guess_lexer_using_modeline(text)
|
||||||
|
|
||||||
if lexer1:
|
if lexer1:
|
||||||
@ -171,6 +169,10 @@ def get_language_from_extension(file_name):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
filepart, extension = os.path.splitext(file_name)
|
filepart, extension = os.path.splitext(file_name)
|
||||||
|
pathpart, filename = os.path.split(file_name)
|
||||||
|
|
||||||
|
if filename == 'go.mod':
|
||||||
|
return 'Go'
|
||||||
|
|
||||||
if re.match(r'\.h.*$', extension, re.IGNORECASE) or re.match(r'\.c.*$', extension, re.IGNORECASE):
|
if re.match(r'\.h.*$', extension, re.IGNORECASE) or re.match(r'\.c.*$', extension, re.IGNORECASE):
|
||||||
|
|
||||||
@ -184,8 +186,12 @@ def get_language_from_extension(file_name):
|
|||||||
return 'Objective-C++'
|
return 'Objective-C++'
|
||||||
|
|
||||||
available_extensions = extensions_in_same_folder(file_name)
|
available_extensions = extensions_in_same_folder(file_name)
|
||||||
if '.cpp' in available_extensions:
|
|
||||||
return 'C++'
|
for ext in CppLexer.filenames:
|
||||||
|
ext = ext.lstrip('*')
|
||||||
|
if ext in available_extensions:
|
||||||
|
return 'C++'
|
||||||
|
|
||||||
if '.c' in available_extensions:
|
if '.c' in available_extensions:
|
||||||
return 'C'
|
return 'C'
|
||||||
|
|
||||||
@ -222,22 +228,21 @@ def number_lines_in_file(file_name):
|
|||||||
def standardize_language(language, plugin):
|
def standardize_language(language, plugin):
|
||||||
"""Maps a string to the equivalent Pygments language.
|
"""Maps a string to the equivalent Pygments language.
|
||||||
|
|
||||||
Returns a tuple of (language_str, lexer_obj).
|
Returns the standardized language string.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if not language:
|
if not language:
|
||||||
return None, None
|
return None
|
||||||
|
|
||||||
# standardize language for this plugin
|
# standardize language for this plugin
|
||||||
if plugin:
|
if plugin:
|
||||||
plugin = plugin.split(' ')[-1].split('/')[0].split('-')[0]
|
plugin = plugin.split(' ')[-1].split('/')[0].split('-')[0]
|
||||||
standardized = get_language_from_json(language, plugin)
|
standardized = get_language_from_json(language, plugin)
|
||||||
if standardized is not None:
|
if standardized is not None:
|
||||||
return standardized, get_lexer(standardized)
|
return standardized
|
||||||
|
|
||||||
# standardize language against default languages
|
# standardize language against default languages
|
||||||
standardized = get_language_from_json(language, 'default')
|
return get_language_from_json(language, 'default')
|
||||||
return standardized, get_lexer(standardized)
|
|
||||||
|
|
||||||
|
|
||||||
def get_lexer(language):
|
def get_lexer(language):
|
||||||
|
@ -26,6 +26,7 @@ log = logging.getLogger('WakaTime')
|
|||||||
|
|
||||||
BACKSLASH_REPLACE_PATTERN = re.compile(r'[\\/]+')
|
BACKSLASH_REPLACE_PATTERN = re.compile(r'[\\/]+')
|
||||||
WINDOWS_DRIVE_PATTERN = re.compile(r'^[a-z]:/')
|
WINDOWS_DRIVE_PATTERN = re.compile(r'^[a-z]:/')
|
||||||
|
WINDOWS_NETWORK_MOUNT_PATTERN = re.compile(r'^\\{2}[a-z]+', re.IGNORECASE)
|
||||||
|
|
||||||
|
|
||||||
def should_exclude(entity, include, exclude):
|
def should_exclude(entity, include, exclude):
|
||||||
@ -77,10 +78,16 @@ def format_file_path(filepath):
|
|||||||
"""Formats a path as absolute and with the correct platform separator."""
|
"""Formats a path as absolute and with the correct platform separator."""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
is_windows_network_mount = WINDOWS_NETWORK_MOUNT_PATTERN.match(filepath)
|
||||||
filepath = os.path.realpath(os.path.abspath(filepath))
|
filepath = os.path.realpath(os.path.abspath(filepath))
|
||||||
filepath = re.sub(BACKSLASH_REPLACE_PATTERN, '/', filepath)
|
filepath = re.sub(BACKSLASH_REPLACE_PATTERN, '/', filepath)
|
||||||
if WINDOWS_DRIVE_PATTERN.match(filepath):
|
is_windows_drive = WINDOWS_DRIVE_PATTERN.match(filepath)
|
||||||
|
if is_windows_drive:
|
||||||
filepath = filepath.capitalize()
|
filepath = filepath.capitalize()
|
||||||
|
if is_windows_network_mount:
|
||||||
|
# Add back a / to the front, since the previous modifications
|
||||||
|
# will have replaced any double slashes with single
|
||||||
|
filepath = '/' + filepath
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
return filepath
|
return filepath
|
||||||
|
Reference in New Issue
Block a user