ghostnotes 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ghostnotes/__init__.py +0 -0
- ghostnotes/cli.py +54 -0
- ghostnotes/config.py +74 -0
- ghostnotes/hook.py +103 -0
- ghostnotes/sync.py +140 -0
- ghostnotes-1.0.0.dist-info/METADATA +56 -0
- ghostnotes-1.0.0.dist-info/RECORD +11 -0
- ghostnotes-1.0.0.dist-info/WHEEL +5 -0
- ghostnotes-1.0.0.dist-info/entry_points.txt +2 -0
- ghostnotes-1.0.0.dist-info/licenses/LICENSE +21 -0
- ghostnotes-1.0.0.dist-info/top_level.txt +1 -0
ghostnotes/__init__.py
ADDED
|
File without changes
|
ghostnotes/cli.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# CLI setup
|
|
2
|
+
import argparse
|
|
3
|
+
from ghostnotes.hook import install_hook, strip_ghostnotes
|
|
4
|
+
from ghostnotes.config import create_config, update_exclude, set_tag, add_lang_support
|
|
5
|
+
from ghostnotes.sync import pull, extract_notes
|
|
6
|
+
|
|
7
|
+
def main():
|
|
8
|
+
parser = argparse.ArgumentParser(description="GhostNotes")
|
|
9
|
+
subparsers = parser.add_subparsers(dest="command")
|
|
10
|
+
|
|
11
|
+
# init
|
|
12
|
+
init_parser = subparsers.add_parser("init", help="Initialize GhostNotes")
|
|
13
|
+
|
|
14
|
+
# set tag
|
|
15
|
+
tag_parser = subparsers.add_parser("set-tag", help="Set the tag for GhostNotes to ignore, Default tag is 'GN:'. ")
|
|
16
|
+
tag_parser.add_argument("--tag", required=True, help="New tag")
|
|
17
|
+
|
|
18
|
+
# add lang
|
|
19
|
+
lang_parser = subparsers.add_parser("add-lang", help="Add a new language support, Check .ghostnotes for the default languages supported")
|
|
20
|
+
lang_parser.add_argument("--ext", required=True, help="File extension (.py, .js, .sql etc.)")
|
|
21
|
+
lang_parser.add_argument("--symb", required=True, help="Symbol for commenting in the said language (# for .py, // for .java etc.)")
|
|
22
|
+
|
|
23
|
+
# pull
|
|
24
|
+
pull_parser = subparsers.add_parser("pull", help="Git pull with ghostnote-safe stripping and re-applying")
|
|
25
|
+
|
|
26
|
+
# status
|
|
27
|
+
status_parser = subparsers.add_parser("status", help="Show all ghost notes in the project")
|
|
28
|
+
|
|
29
|
+
args = parser.parse_args()
|
|
30
|
+
|
|
31
|
+
if args.command == "init":
|
|
32
|
+
create_config()
|
|
33
|
+
update_exclude()
|
|
34
|
+
install_hook()
|
|
35
|
+
print("GhostNotes is successfully initialized.")
|
|
36
|
+
elif args.command == "set-tag":
|
|
37
|
+
set_tag(args.tag)
|
|
38
|
+
elif args.command == "add-lang":
|
|
39
|
+
add_lang_support(args.ext, args.symb)
|
|
40
|
+
elif args.command == "pull":
|
|
41
|
+
pull()
|
|
42
|
+
elif args.command == "status":
|
|
43
|
+
notes = extract_notes()
|
|
44
|
+
if not notes:
|
|
45
|
+
print("No ghost notes found.")
|
|
46
|
+
else:
|
|
47
|
+
total = sum(len(v) for v in notes.values())
|
|
48
|
+
print(f"Found {total} ghost note(s):\n")
|
|
49
|
+
for file, file_notes in notes.items():
|
|
50
|
+
for n in file_notes:
|
|
51
|
+
print(f" {file}:{n['line_number'] + 1} — {n['note']}")
|
|
52
|
+
|
|
53
|
+
if __name__ == "__main__":
|
|
54
|
+
main()
|
ghostnotes/config.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# file for creating the .ghostnotes configuration
|
|
2
|
+
import configparser
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
def create_config():
|
|
6
|
+
if not os.path.isdir('.git'):
|
|
7
|
+
print("This project is not initialized with git. GhostNotes only works for directories already initialized with git.")
|
|
8
|
+
return
|
|
9
|
+
|
|
10
|
+
else:
|
|
11
|
+
# set-up the default config
|
|
12
|
+
config = configparser.ConfigParser()
|
|
13
|
+
config['settings'] = {'tag': 'GN:'}
|
|
14
|
+
config['languages'] = {'.py': '#', '.java': '//', '.js': '//', '.ts': '//'}
|
|
15
|
+
|
|
16
|
+
with open('.ghostnotes', 'w') as configfile:
|
|
17
|
+
config.write(configfile)
|
|
18
|
+
|
|
19
|
+
print("GhostNotes: Config file is created succesfully")
|
|
20
|
+
|
|
21
|
+
def update_exclude():
|
|
22
|
+
loc = ".git/info/exclude"
|
|
23
|
+
|
|
24
|
+
with open(loc, 'r') as file:
|
|
25
|
+
for line in file:
|
|
26
|
+
# already in exclude
|
|
27
|
+
if line.strip() == '.ghostnotes':
|
|
28
|
+
return
|
|
29
|
+
|
|
30
|
+
file = open(loc, "a")
|
|
31
|
+
file.write('.ghostnotes\n')
|
|
32
|
+
file.close()
|
|
33
|
+
|
|
34
|
+
def load_config():
|
|
35
|
+
# check if config exists
|
|
36
|
+
if not os.path.isfile('.ghostnotes'):
|
|
37
|
+
print("This project is not initialized with GhostNotes.")
|
|
38
|
+
return
|
|
39
|
+
|
|
40
|
+
else:
|
|
41
|
+
# set-up the default config
|
|
42
|
+
config = configparser.ConfigParser()
|
|
43
|
+
config.read('.ghostnotes')
|
|
44
|
+
print("GhostNotes: Config file is read succesfully")
|
|
45
|
+
|
|
46
|
+
return config
|
|
47
|
+
|
|
48
|
+
# update tag
|
|
49
|
+
def set_tag(tag):
|
|
50
|
+
if not os.path.isfile('.ghostnotes'):
|
|
51
|
+
print("This project is not initialized with GhostNotes, cannot set new tag.")
|
|
52
|
+
return
|
|
53
|
+
|
|
54
|
+
config = configparser.ConfigParser()
|
|
55
|
+
config.read('.ghostnotes') # preserve existing settings
|
|
56
|
+
config['settings']['tag'] = tag
|
|
57
|
+
with open('.ghostnotes', 'w') as configfile:
|
|
58
|
+
config.write(configfile)
|
|
59
|
+
|
|
60
|
+
print(f"GhostNotes: Config file tag is set to {tag} succesfully")
|
|
61
|
+
|
|
62
|
+
# add custom language comment support
|
|
63
|
+
def add_lang_support(extension, notation):
|
|
64
|
+
if not os.path.isfile('.ghostnotes'):
|
|
65
|
+
print("This project is not initialized with GhostNotes, cannot add new file support.")
|
|
66
|
+
return
|
|
67
|
+
|
|
68
|
+
config = configparser.ConfigParser()
|
|
69
|
+
config.read('.ghostnotes')
|
|
70
|
+
config['languages'][extension] = notation
|
|
71
|
+
with open('.ghostnotes', 'w') as configfile:
|
|
72
|
+
config.write(configfile)
|
|
73
|
+
|
|
74
|
+
print(f"GhostNotes: Config file language support for {extension} files are added with symbol {notation} successfuly.")
|
ghostnotes/hook.py
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import stat
|
|
3
|
+
import subprocess
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from ghostnotes.config import load_config
|
|
6
|
+
|
|
7
|
+
# for adding this script to pre-commit
|
|
8
|
+
def install_hook():
|
|
9
|
+
path = '.git/hooks/pre-commit'
|
|
10
|
+
command = 'python3 -m ghostnotes.hook'
|
|
11
|
+
|
|
12
|
+
# if a pre-commit hook file already exists:
|
|
13
|
+
if os.path.exists(path):
|
|
14
|
+
with open(path, 'r') as file:
|
|
15
|
+
for line in file:
|
|
16
|
+
if line.strip() == command:
|
|
17
|
+
return
|
|
18
|
+
|
|
19
|
+
with open(path, 'a') as file:
|
|
20
|
+
file.write(f'\n{command}\n')
|
|
21
|
+
else:
|
|
22
|
+
with open(path, 'w') as file:
|
|
23
|
+
file.write(f'#!/bin/bash\n{command}\n')
|
|
24
|
+
|
|
25
|
+
# make the file executable
|
|
26
|
+
os.chmod(path, os.stat(path).st_mode | stat.S_IEXEC)
|
|
27
|
+
|
|
28
|
+
# main logic function for stripping the committed files of the keyworded comments
|
|
29
|
+
def strip_ghostnotes():
|
|
30
|
+
"""
|
|
31
|
+
Read Config
|
|
32
|
+
Get the list of staged files — how do you ask git which files are staged? Think about what terminal command you'd run manually.
|
|
33
|
+
Filter to only supported extensions
|
|
34
|
+
For each supported file, strip lines matching the tag.
|
|
35
|
+
Re-stage the files with git add
|
|
36
|
+
"""
|
|
37
|
+
# read config
|
|
38
|
+
configuration = load_config()
|
|
39
|
+
|
|
40
|
+
if configuration is None:
|
|
41
|
+
print("Ghostnotes is not configured.")
|
|
42
|
+
return
|
|
43
|
+
|
|
44
|
+
# get the list of staged file:
|
|
45
|
+
res = subprocess.run(['git', 'diff', '--name-only', '--cached'], capture_output=True, text=True)
|
|
46
|
+
staged_files = res.stdout.splitlines()
|
|
47
|
+
supported_staged_files = list()
|
|
48
|
+
|
|
49
|
+
# filter staged_files to only supported file types
|
|
50
|
+
for f in staged_files:
|
|
51
|
+
if Path(f).suffix in configuration['languages']:
|
|
52
|
+
supported_staged_files.append(f)
|
|
53
|
+
|
|
54
|
+
# strip lines matching the tag
|
|
55
|
+
for file in supported_staged_files:
|
|
56
|
+
# check the commenting style for the file type from configuration['languages'] as well as the tag from configuration['settings'] to build the exact sequence of chars we are looking for
|
|
57
|
+
comment = configuration['languages'][Path(file).suffix]
|
|
58
|
+
tag = configuration['settings']['tag']
|
|
59
|
+
pattern = comment + ' ' + tag
|
|
60
|
+
|
|
61
|
+
# read the staged version from the git index, not the working tree
|
|
62
|
+
staged_content = subprocess.run(
|
|
63
|
+
['git', 'show', f':{file}'],
|
|
64
|
+
capture_output=True, text=True
|
|
65
|
+
).stdout
|
|
66
|
+
|
|
67
|
+
new_lines = list()
|
|
68
|
+
for line in staged_content.splitlines(keepends=True):
|
|
69
|
+
# strip only if the pattern is not inside a string literal
|
|
70
|
+
if pattern in line:
|
|
71
|
+
idx = line.index(pattern)
|
|
72
|
+
prefix = line[:idx]
|
|
73
|
+
# if an odd number of quotes precede the pattern, it's inside a string
|
|
74
|
+
in_string = (prefix.count('"') % 2 == 1) or (prefix.count("'") % 2 == 1)
|
|
75
|
+
if not in_string:
|
|
76
|
+
line = line[:idx].rstrip() + '\n'
|
|
77
|
+
new_lines.append(line)
|
|
78
|
+
|
|
79
|
+
stripped_content = ''.join(new_lines)
|
|
80
|
+
|
|
81
|
+
# write the stripped content back into the git index only
|
|
82
|
+
hash_result = subprocess.run(
|
|
83
|
+
['git', 'hash-object', '-w', '--stdin'],
|
|
84
|
+
input=stripped_content, capture_output=True, text=True
|
|
85
|
+
)
|
|
86
|
+
blob_hash = hash_result.stdout.strip()
|
|
87
|
+
|
|
88
|
+
# get the file's mode from the index
|
|
89
|
+
ls_result = subprocess.run(
|
|
90
|
+
['git', 'ls-files', '--stage', file],
|
|
91
|
+
capture_output=True, text=True
|
|
92
|
+
)
|
|
93
|
+
if not ls_result.stdout.strip():
|
|
94
|
+
continue
|
|
95
|
+
mode = ls_result.stdout.split()[0]
|
|
96
|
+
|
|
97
|
+
# update the index entry with the new blob
|
|
98
|
+
subprocess.run(
|
|
99
|
+
['git', 'update-index', '--cacheinfo', f'{mode},{blob_hash},{file}']
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
if __name__ == "__main__":
|
|
103
|
+
strip_ghostnotes()
|
ghostnotes/sync.py
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from ghostnotes.config import load_config
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def extract_notes():
|
|
7
|
+
config = load_config()
|
|
8
|
+
notes = {}
|
|
9
|
+
|
|
10
|
+
for file in Path('.').rglob('*'):
|
|
11
|
+
# skip hidden directories (.git, .venv, etc.)
|
|
12
|
+
if any(part.startswith('.') for part in file.parts):
|
|
13
|
+
continue
|
|
14
|
+
|
|
15
|
+
if not file.is_file():
|
|
16
|
+
continue
|
|
17
|
+
|
|
18
|
+
ext = file.suffix
|
|
19
|
+
if ext not in config['languages']:
|
|
20
|
+
continue
|
|
21
|
+
|
|
22
|
+
comment = config['languages'][ext]
|
|
23
|
+
tag = config['settings']['tag']
|
|
24
|
+
pattern = comment + ' ' + tag
|
|
25
|
+
|
|
26
|
+
# extract notes, store code without comment, store comment, and store line num
|
|
27
|
+
file_notes = []
|
|
28
|
+
try:
|
|
29
|
+
with open(file, 'r') as f:
|
|
30
|
+
for i, line in enumerate(f):
|
|
31
|
+
if pattern in line:
|
|
32
|
+
stripped = line.split(pattern)[0].rstrip()
|
|
33
|
+
note_text = line.split(pattern, 1)[1].strip()
|
|
34
|
+
file_notes.append({
|
|
35
|
+
'stripped_line': stripped,
|
|
36
|
+
'note': note_text,
|
|
37
|
+
'line_number': i,
|
|
38
|
+
})
|
|
39
|
+
except UnicodeDecodeError:
|
|
40
|
+
continue
|
|
41
|
+
|
|
42
|
+
if file_notes:
|
|
43
|
+
notes[str(file)] = file_notes
|
|
44
|
+
|
|
45
|
+
return notes
|
|
46
|
+
|
|
47
|
+
def strip_working_tree(notes):
|
|
48
|
+
for file, file_notes in notes.items():
|
|
49
|
+
patterns = {n['line_number'] for n in file_notes}
|
|
50
|
+
|
|
51
|
+
with open(file, 'r') as f:
|
|
52
|
+
lines = f.readlines()
|
|
53
|
+
|
|
54
|
+
for line_num in patterns:
|
|
55
|
+
note_entry = next(n for n in file_notes if n['line_number'] == line_num)
|
|
56
|
+
lines[line_num] = note_entry['stripped_line'] + '\n'
|
|
57
|
+
|
|
58
|
+
with open(file, 'w') as f:
|
|
59
|
+
f.writelines(lines)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def reapply_notes(notes, config):
|
|
63
|
+
for file, file_notes in notes.items():
|
|
64
|
+
if not Path(file).is_file():
|
|
65
|
+
for n in file_notes:
|
|
66
|
+
print(f" ORPHANED (file deleted): {file} — {n['note']}")
|
|
67
|
+
continue
|
|
68
|
+
|
|
69
|
+
comment = config['languages'][Path(file).suffix]
|
|
70
|
+
tag = config['settings']['tag']
|
|
71
|
+
pattern = comment + ' ' + tag
|
|
72
|
+
|
|
73
|
+
with open(file, 'r') as f:
|
|
74
|
+
lines = f.readlines()
|
|
75
|
+
|
|
76
|
+
orphaned = []
|
|
77
|
+
|
|
78
|
+
for n in file_notes:
|
|
79
|
+
target = n['stripped_line']
|
|
80
|
+
line_num = n['line_number']
|
|
81
|
+
matched = False
|
|
82
|
+
|
|
83
|
+
# 1. exact match at same line number
|
|
84
|
+
if line_num < len(lines) and lines[line_num].rstrip() == target:
|
|
85
|
+
lines[line_num] = target + ' ' + pattern + ' ' + n['note'] + '\n'
|
|
86
|
+
matched = True
|
|
87
|
+
|
|
88
|
+
# 2. exact match nearby (within 20 lines)
|
|
89
|
+
if not matched:
|
|
90
|
+
start = max(0, line_num - 20)
|
|
91
|
+
end = min(len(lines), line_num + 20)
|
|
92
|
+
for i in range(start, end):
|
|
93
|
+
if lines[i].rstrip() == target:
|
|
94
|
+
lines[i] = target + ' ' + pattern + ' ' + n['note'] + '\n'
|
|
95
|
+
matched = True
|
|
96
|
+
break
|
|
97
|
+
|
|
98
|
+
# 3. exact match anywhere in file
|
|
99
|
+
if not matched:
|
|
100
|
+
for i in range(len(lines)):
|
|
101
|
+
if lines[i].rstrip() == target:
|
|
102
|
+
lines[i] = target + ' ' + pattern + ' ' + n['note'] + '\n'
|
|
103
|
+
matched = True
|
|
104
|
+
break
|
|
105
|
+
|
|
106
|
+
# 4. orphaned
|
|
107
|
+
if not matched:
|
|
108
|
+
orphaned.append(n)
|
|
109
|
+
|
|
110
|
+
with open(file, 'w') as f:
|
|
111
|
+
f.writelines(lines)
|
|
112
|
+
|
|
113
|
+
for n in orphaned:
|
|
114
|
+
print(f" ORPHANED: {file}:{n['line_number']} — {n['note']}")
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def pull():
|
|
118
|
+
config = load_config()
|
|
119
|
+
notes = extract_notes()
|
|
120
|
+
|
|
121
|
+
if notes:
|
|
122
|
+
print(f"GhostNotes: Saved {sum(len(v) for v in notes.values())} note(s), stripping before pull...")
|
|
123
|
+
strip_working_tree(notes)
|
|
124
|
+
|
|
125
|
+
result = subprocess.run(['git', 'pull'], capture_output=True, text=True)
|
|
126
|
+
print(result.stdout)
|
|
127
|
+
if result.stderr:
|
|
128
|
+
print(result.stderr)
|
|
129
|
+
|
|
130
|
+
if notes:
|
|
131
|
+
if result.returncode != 0:
|
|
132
|
+
print("GhostNotes: Pull failed, restoring notes to working tree...")
|
|
133
|
+
reapply_notes(notes, config)
|
|
134
|
+
print("GhostNotes: Notes restored. Resolve the pull issue and try again.")
|
|
135
|
+
else:
|
|
136
|
+
print("GhostNotes: Re-applying notes...")
|
|
137
|
+
reapply_notes(notes, config)
|
|
138
|
+
print("GhostNotes: Done.")
|
|
139
|
+
|
|
140
|
+
return result.returncode
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ghostnotes
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Strip personal annotations from code before committing
|
|
5
|
+
Author-email: Tuna Akin <tuna.takin@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Requires-Python: >=3.8
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Dynamic: license-file
|
|
11
|
+
|
|
12
|
+
# GhostNotes
|
|
13
|
+
**Version:** 1.0.0 | **Last Updated:** April 12, 2026
|
|
14
|
+
|
|
15
|
+
A Git tool that strips tagged comments before committing.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Installation
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pip install ghostnotes
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Then initialize in any git repo:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
cd your-project
|
|
29
|
+
ghostnotes init
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## What it does
|
|
35
|
+
GhostNotes lets you leave personal notes in your code using a tag (default: `GN:`). Before every commit, it automatically strips them out so they never reach your repository.
|
|
36
|
+
```python
|
|
37
|
+
x = some_function() # GN: this breaks when input is negative, fix later
|
|
38
|
+
```
|
|
39
|
+
This comment line gets stripped before the commit so it looks like:
|
|
40
|
+
```python
|
|
41
|
+
x = some_function()
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Your local file stays untouched.
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## Commands
|
|
49
|
+
|
|
50
|
+
| Command | Description |
|
|
51
|
+
|---|---|
|
|
52
|
+
| `ghostnotes init` | Initialize GhostNotes in the current project |
|
|
53
|
+
| `ghostnotes status` | Show all GhostNotes in the current project |
|
|
54
|
+
| `ghostnotes set-tag --tag <tag>` | Change the default comment tag |
|
|
55
|
+
| `ghostnotes add-lang --ext <.ext> --symb <symbol>` | Add support for a new language |
|
|
56
|
+
| `ghostnotes pull` | Git pull that safely strips and re-applies your ghostnotes |
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
ghostnotes/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
ghostnotes/cli.py,sha256=mSpnu9y1oLkShcbH6Z1IEh0XAr6n3nC6CQ1KS82LvGQ,2116
|
|
3
|
+
ghostnotes/config.py,sha256=CYBoSVkcTn_-IZQuhjlY4HqqIRLrvNlr2_nLl637bRk,2398
|
|
4
|
+
ghostnotes/hook.py,sha256=PnhyxuAZJXXG_uJRl7A59K6yAQHan7dWgmkhgyoZ7-E,3740
|
|
5
|
+
ghostnotes/sync.py,sha256=WpAdVJmH5d-xHS7Luov-mijwByS3I_OaFq3qXvRKptE,4477
|
|
6
|
+
ghostnotes-1.0.0.dist-info/licenses/LICENSE,sha256=nOQImqRAvHs2zvX_672bmFNV8j1E34KEVWXKMLc_YB8,1066
|
|
7
|
+
ghostnotes-1.0.0.dist-info/METADATA,sha256=oKOtOzVMYZhiJkq0-Eu9cVwoi-wuSX92C9gsH9SKt5E,1413
|
|
8
|
+
ghostnotes-1.0.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
9
|
+
ghostnotes-1.0.0.dist-info/entry_points.txt,sha256=ePr6r5ZXsebGdIkj_q_hcD8_qkWkiir-PTYCdfHBolc,51
|
|
10
|
+
ghostnotes-1.0.0.dist-info/top_level.txt,sha256=KgbY9qlqn3rtYmhkjV4cnWFn1qReukDtWfyUh8Tbzxg,11
|
|
11
|
+
ghostnotes-1.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Tuna Akin
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ghostnotes
|