Skip to content

Commit 5f31bed

Browse files
committed
🚚 Move __init__.py to hooks/wakatime.py
1 parent c958258 commit 5f31bed

File tree

16 files changed

+332
-87
lines changed

16 files changed

+332
-87
lines changed

.github/FUNDING.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
---
2+
patreon: user?u=83975719
23
custom:
34
- "https://user-images.githubusercontent.com/32936898/199681341-1c5cfa61-4411-4b67-b268-7cd87c5867bb.png"
45
- "https://user-images.githubusercontent.com/32936898/199681363-1094a0be-85ca-49cf-a410-19b3d7965120.png"

.pre-commit-config.yaml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ repos:
3838
args:
3939
- --msg-filename
4040
- repo: https://github.com/editorconfig-checker/editorconfig-checker.python
41-
rev: 2.7.2
41+
rev: 2.7.3
4242
hooks:
4343
- id: editorconfig-checker
4444
- repo: https://github.com/jumanjihouse/pre-commit-hooks
@@ -50,7 +50,7 @@ repos:
5050
hooks:
5151
- id: actionlint
5252
- repo: https://github.com/adrienverge/yamllint
53-
rev: v1.32.0
53+
rev: v1.33.0
5454
hooks:
5555
- id: yamllint
5656
- repo: https://github.com/executablebooks/mdformat
@@ -71,14 +71,14 @@ repos:
7171
hooks:
7272
- id: markdownlint-cli2
7373
additional_dependencies:
74-
- markdown-it-texmath@0.9.1
74+
- markdown-it-texmath
7575
- repo: https://github.com/Freed-Wu/pre-commit-hooks
7676
rev: 0.0.11
7777
hooks:
7878
- id: update-CITATION.cff
7979
- id: update-pyproject.toml
8080
- repo: https://github.com/psf/black
81-
rev: 23.9.1
81+
rev: 23.11.0
8282
hooks:
8383
- id: black
8484
- repo: https://github.com/PyCQA/isort
@@ -92,7 +92,7 @@ repos:
9292
additional_dependencies:
9393
- tomli
9494
- repo: https://github.com/kumaraditya303/mirrors-pyright
95-
rev: v1.1.329
95+
rev: v1.1.335
9696
hooks:
9797
- id: pyright
9898
- repo: https://github.com/PyCQA/bandit

README.md

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -94,11 +94,18 @@ at the end of file.
9494
```python
9595
from repl_python_wakatime.python import install_hook
9696

97-
install_hook(f, args, kwargs)
97+
install_hook(hook_function, args, kwargs)
9898
```
9999

100-
will execute `f(*args, **kwargs)` after every output/input. Other REPLs are
101-
similar.
100+
will execute `hook_function(*args, **kwargs)` after every output/input. Other
101+
REPLs are similar. Currently, `hook_function` can be:
102+
103+
- `repl_python_wakatime.hooks.wakatime.wakatime_hook()`: By default.
104+
- `repl_python_wakatime.hooks.codestats.codestats_hook()`: for [codestats](https://codestats.net/)
105+
- Create your hooks for other similar projects, such as:
106+
- [codetime](https://codetime.dev/)
107+
- [rescuetime](https://www.rescuetime.com/)
108+
- ...
102109

103110
## Similar projects
104111

pyproject.toml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,9 @@ file = "requirements/dev.txt"
4848
[tool.setuptools.dynamic.optional-dependencies.ipython]
4949
file = "requirements/ipython.txt"
5050

51+
[tool.setuptools.dynamic.optional-dependencies.keyring]
52+
file = "requirements/keyring.txt"
53+
5154
[tool.setuptools.dynamic.optional-dependencies.ptipython]
5255
file = "requirements/ptipython.txt"
5356

@@ -88,5 +91,5 @@ exclude_lines = [
8891
[tool.bandit.assert_used]
8992
skips = [
9093
"*_test.py",
91-
"test_*.py",
94+
"*/test_*.py",
9295
]

requirements/dev.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
#!/usr/bin/env -S pip install -r
22
# For unit test and code coverage rate test.
33

4-
pre-commit
4+
keyring
55
ptpython[ptipython]
66
pytest-cov

requirements/keyring.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
#!/usr/bin/env -S pip install -r
2+
# Store your codestats API key to `keyring get codestats HOST_NAME`.
3+
4+
keyring

src/repl_python_wakatime/__init__.py

Lines changed: 5 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -1,74 +1,10 @@
1-
"""Refer `create-plugin <https://wakatime.com/help/creating-plugin>`_."""
2-
import logging
3-
import os
4-
from shutil import which
5-
from subprocess import run # nosec: B404
6-
from threading import Thread
7-
from typing import Any, Callable
8-
1+
r"""Provide ``__version__`` for
2+
`importlib.metadata.version() <https://docs.python.org/3/library/importlib.metadata.html#distribution-versions>`_.
3+
"""
94
try:
105
from ._version import __version__, __version_tuple__ # type: ignore
11-
except ImportError:
6+
except ImportError: # for setuptools-generate
127
__version__ = "rolling"
138
__version_tuple__ = (0, 0, 0, __version__, "")
149

15-
logger = logging.getLogger(__name__)
16-
17-
18-
def send_wakatime_heartbeat(
19-
project: str = "",
20-
category: str = "coding",
21-
plugin: str = "repl-python-wakatime",
22-
filenames: list[str] = [".git"],
23-
detect_func: Callable[[str], bool] = os.path.isdir,
24-
) -> None:
25-
"""Send wakatime heartbeat.
26-
27-
If ``project == ""``, detect automatically.
28-
29-
``plugin`` must be the format of ``repl-REPL_NAME-wakatime`` to let
30-
wakatime detect correctly.
31-
32-
:param project:
33-
:type project: str
34-
:param category:
35-
:type category: str
36-
:param plugin:
37-
:type plugin: str
38-
:param filenames:
39-
:type filenames: list[str]
40-
:param detect_func:
41-
:type detect_func: Callable[[str], bool]
42-
:rtype: None
43-
"""
44-
if project == "":
45-
from .utils import get_project
46-
47-
project = get_project(filenames, detect_func)
48-
run( # nosec: B603 B607
49-
[
50-
"wakatime-cli",
51-
"--write",
52-
f"--category={category}",
53-
f"--plugin={plugin}",
54-
"--entity-type=app",
55-
"--entity=python",
56-
"--alternate-language=python",
57-
f"--project={project}",
58-
],
59-
stdout=open(os.devnull, "w"),
60-
)
61-
62-
63-
def wakatime_hook(*args: Any, **kwargs: Any) -> None:
64-
"""Wakatime hook.
65-
66-
:param args:
67-
:type args: Any
68-
:param kwargs:
69-
:type kwargs: Any
70-
:rtype: None
71-
"""
72-
task = Thread(target=send_wakatime_heartbeat, args=args, kwargs=kwargs)
73-
task.daemon = True
74-
task.start()
10+
__all__ = ["__version__", "__version_tuple__"]
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
"""Codestats
2+
============
3+
4+
Refer `code-stats-vim
5+
<https://gitlab.com/code-stats/code-stats-vim/-/blob/master/pythonx/codestats.py>
6+
`_.
7+
"""
8+
import json
9+
import logging
10+
import threading
11+
import time
12+
from datetime import datetime
13+
from http.client import HTTPException
14+
from socket import gethostname
15+
from ssl import CertificateError
16+
from typing import NoReturn
17+
from urllib.error import URLError
18+
from urllib.request import Request, urlopen
19+
20+
logger = logging.getLogger(__name__)
21+
INTERVAL = 10 # interval at which stats are sent
22+
SLEEP_INTERVAL = 0.1 # sleep interval for timeslicing
23+
TIMEOUT = 2 # request timeout value (in seconds)
24+
25+
codestats = None
26+
27+
28+
def codestats_hook(
29+
api_key: str = "",
30+
url: str = "https://codestats.net/api/my/pulses",
31+
language_type: str = "Terminal (python)",
32+
service_name: str = "codestats",
33+
user_name: str = gethostname(),
34+
) -> None:
35+
"""Codestats hook.
36+
37+
:param api_key:
38+
:type api_key: str
39+
:param url:
40+
:type url: str
41+
:param language_type:
42+
:type language_type: str
43+
:param service_name:
44+
:type service_name: str
45+
:param user_name:
46+
:type user_name: str
47+
:rtype: None
48+
"""
49+
global codestats
50+
if codestats is None:
51+
if api_key == "":
52+
from ..utils.api import get_api_key
53+
54+
api_key = get_api_key(service_name, user_name)
55+
codestats = CodeStats(api_key, url, language_type)
56+
codestats.add_xp()
57+
58+
59+
class CodeStats:
60+
"""Codestats."""
61+
62+
def __init__(
63+
self,
64+
api_key: str,
65+
url: str = "https://codestats.net/api/my/pulses",
66+
language_type: str = "Terminal (python)",
67+
) -> None:
68+
"""Init.
69+
70+
:param api_key:
71+
:type api_key: str
72+
:param url:
73+
:type url: str
74+
:param language_type:
75+
:type language_type: str
76+
:rtype: None
77+
"""
78+
self.url = url
79+
self.api_key = api_key
80+
self.language_type = language_type
81+
self.xp_dict = {language_type: 0}
82+
83+
self.sem = threading.Semaphore()
84+
85+
self.cs_thread = threading.Thread(target=self.main_thread)
86+
self.cs_thread.daemon = True
87+
self.cs_thread.start()
88+
89+
def add_xp(self, xp: int = 1) -> None:
90+
"""Add xp.
91+
92+
Sem sections are super small so this should be quick if it blocks.
93+
94+
:param xp:
95+
:type xp: int
96+
:rtype: None
97+
"""
98+
self.sem.acquire()
99+
self.xp_dict[self.language_type] += xp
100+
self.sem.release()
101+
102+
def send_xp(self) -> None:
103+
"""Send xp.
104+
105+
Acquire the lock to get the list of xp to send.
106+
107+
:rtype: None
108+
"""
109+
if len(self.xp_dict) == 0:
110+
return
111+
112+
self.sem.acquire()
113+
xp_list = [dict(language=ft, xp=xp) for ft, xp in self.xp_dict.items()]
114+
self.xp_dict = {self.language_type: 0}
115+
self.sem.release()
116+
117+
headers = {
118+
"Content-Type": "application/json",
119+
"User-Agent": "code-stats-python/{0}".format(__version__),
120+
"X-API-Token": self.api_key,
121+
"Accept": "*/*",
122+
}
123+
124+
# after lock is released we can send the payload
125+
utc_now = datetime.now().astimezone().isoformat()
126+
pulse_json = json.dumps(
127+
{"coded_at": "{0}".format(utc_now), "xps": xp_list}
128+
).encode("utf-8")
129+
req = Request(url=self.url, data=pulse_json, headers=headers)
130+
error = ""
131+
try:
132+
response = urlopen(req, timeout=TIMEOUT) # nosec: B310
133+
response.read()
134+
# connection might not be closed without .read()
135+
except URLError as e:
136+
try:
137+
# HTTP error
138+
error = "{0} {1}".format(
139+
e.code, # type: ignore
140+
e.read().decode("utf-8"), # type: ignore
141+
)
142+
except AttributeError:
143+
# non-HTTP error, eg. no network
144+
error = e.reason
145+
except CertificateError as e:
146+
# SSL certificate error (eg. a public wifi redirects traffic)
147+
error = e
148+
except HTTPException as e:
149+
error = "HTTPException on send data. Msg: {0}\nDoc?:{1}".format(
150+
e.message, e.__doc__ # type: ignore
151+
)
152+
if error:
153+
logger.error(error)
154+
155+
def main_thread(self) -> NoReturn:
156+
"""Run main thread.
157+
158+
Needs to be able to send XP at an interval.
159+
"""
160+
while True:
161+
cur_time = 0
162+
while cur_time < INTERVAL:
163+
time.sleep(SLEEP_INTERVAL)
164+
cur_time += SLEEP_INTERVAL
165+
166+
self.send_xp()

0 commit comments

Comments
 (0)