panopticas 0.0.9__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.
- panopticas-0.0.9.dist-info/LICENSE +21 -0
- panopticas-0.0.9.dist-info/METADATA +72 -0
- panopticas-0.0.9.dist-info/RECORD +8 -0
- panopticas-0.0.9.dist-info/WHEEL +5 -0
- panopticas-0.0.9.dist-info/entry_points.txt +2 -0
- panopticas-0.0.9.dist-info/top_level.txt +2 -0
- panopticas.py +436 -0
- panopticas_cli.py +111 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Peter Freiberg
|
|
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,72 @@
|
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
|
+
Name: panopticas
|
|
3
|
+
Version: 0.0.9
|
|
4
|
+
Summary: The panopticas CLI tool.
|
|
5
|
+
Author-email: Peter Freiberg <peter.freiberg@gmail.com>
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2024 Peter Freiberg
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
|
|
28
|
+
Project-URL: Homepage, https://panopticas.io
|
|
29
|
+
Project-URL: Issues, https://github.com/kospex/panopticas/issues
|
|
30
|
+
Project-URL: Repository, https://github.com/kospex/panopticas.git
|
|
31
|
+
Keywords: analytics,git
|
|
32
|
+
Classifier: Development Status :: 3 - Alpha
|
|
33
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
34
|
+
Classifier: Programming Language :: Python
|
|
35
|
+
Requires-Python: >=3.11
|
|
36
|
+
Description-Content-Type: text/markdown
|
|
37
|
+
License-File: LICENSE
|
|
38
|
+
Requires-Dist: Click
|
|
39
|
+
Requires-Dist: prettytable
|
|
40
|
+
Requires-Dist: pathspec
|
|
41
|
+
|
|
42
|
+
# panopticas
|
|
43
|
+
|
|
44
|
+
Discover insights into the types of data and functions used in your code.
|
|
45
|
+
|
|
46
|
+
Inspired by tools like [enry](https://github.com/go-enry/go-enry) and [linguist](https://github.com/github-linguist/linguist)
|
|
47
|
+
|
|
48
|
+
Initial feaures: language detector and metadata identifiers, based on the filename extension, filename details and the shebang line.
|
|
49
|
+
|
|
50
|
+
The official documentation can be found at [panopticas.io](https://panopticas.io)
|
|
51
|
+
|
|
52
|
+
## Installation
|
|
53
|
+
|
|
54
|
+
> pip install panopticas
|
|
55
|
+
|
|
56
|
+
## Usage
|
|
57
|
+
|
|
58
|
+
Change into the directory you want check the file types of and then run
|
|
59
|
+
|
|
60
|
+
> panopticas assess
|
|
61
|
+
|
|
62
|
+
To check a single file and get some metadata
|
|
63
|
+
|
|
64
|
+
> panopticas file FILENAME
|
|
65
|
+
|
|
66
|
+
## Development
|
|
67
|
+
|
|
68
|
+
If you want to check out the [panopitcas repo](https://github.com/kospex/panopticas) and work on bug fixes, use the pip "editable" install to set up the _panopticas_ CLI for your shell using:
|
|
69
|
+
> pip install -e .
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
panopticas.py,sha256=gYg5DNyV9w6OIdoQD-lODZIQQ1zEGajPteoP9loZ1fs,11592
|
|
2
|
+
panopticas_cli.py,sha256=Es4p_l0Tdsf2YXN7HK48iCtLjJ-f8tKXVBGjlefqULw,3472
|
|
3
|
+
panopticas-0.0.9.dist-info/LICENSE,sha256=jxVEmRQitTDmkgtGeuVkMkGS4tqq553njlJnp_PpMbQ,1071
|
|
4
|
+
panopticas-0.0.9.dist-info/METADATA,sha256=0p_6ArQWc2KCO31Uh2ZdYF_PRX-J9Fcn0pS2DeAHMBU,2776
|
|
5
|
+
panopticas-0.0.9.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
|
|
6
|
+
panopticas-0.0.9.dist-info/entry_points.txt,sha256=buvlxCprd7u_QCF4vTI6w75GpENtBa5YkDngKjwGgqY,50
|
|
7
|
+
panopticas-0.0.9.dist-info/top_level.txt,sha256=zUI8atAFe07KkXk9JvE7l8YXZxk5RJRx2Ypc6Eg062E,26
|
|
8
|
+
panopticas-0.0.9.dist-info/RECORD,,
|
panopticas.py
ADDED
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Analysis functions for Panopticas.
|
|
3
|
+
"""
|
|
4
|
+
import os
|
|
5
|
+
import re
|
|
6
|
+
import pathspec
|
|
7
|
+
|
|
8
|
+
VERSION = "0.0.8"
|
|
9
|
+
|
|
10
|
+
EXT_FILETYPES = {
|
|
11
|
+
'.c': 'C',
|
|
12
|
+
'.cpp': 'C++',
|
|
13
|
+
'.cs': 'C#',
|
|
14
|
+
'.css': 'CSS',
|
|
15
|
+
'.csv': 'CSV',
|
|
16
|
+
'.dockerignore': 'Dockerignore',
|
|
17
|
+
'.gitignore': 'Gitignore',
|
|
18
|
+
'.gitattributes': 'GitAttributes',
|
|
19
|
+
'.go': 'Go',
|
|
20
|
+
'.gif': "GIF",
|
|
21
|
+
'.h': 'C Header',
|
|
22
|
+
'.htm': 'HTML',
|
|
23
|
+
'.html': 'HTML',
|
|
24
|
+
'.ico': 'ICO',
|
|
25
|
+
'.ini': 'INI',
|
|
26
|
+
'.ipynb': 'Jupyter Notebook',
|
|
27
|
+
'.java': 'Java',
|
|
28
|
+
'.jpg': 'JPEG',
|
|
29
|
+
'.jpeg': 'JPEG',
|
|
30
|
+
'.js': 'JavaScript',
|
|
31
|
+
'.json': 'JSON',
|
|
32
|
+
'.jsx': 'JSX',
|
|
33
|
+
'.kt': 'Kotlin',
|
|
34
|
+
'.m': 'Objective-C',
|
|
35
|
+
'.mailmap': 'Mailmap',
|
|
36
|
+
'.md': 'Markdown',
|
|
37
|
+
'.nvmrc': "nvmrc",
|
|
38
|
+
'.php': 'PHP',
|
|
39
|
+
'.pl': 'Perl',
|
|
40
|
+
'.pm': 'Perl',
|
|
41
|
+
'.png': 'PNG',
|
|
42
|
+
'.py': 'Python',
|
|
43
|
+
'.r': 'R',
|
|
44
|
+
'.rb': 'Ruby',
|
|
45
|
+
'.rs': 'Rust',
|
|
46
|
+
'.rst': 'ReStructuredText',
|
|
47
|
+
'.scala': 'Scala',
|
|
48
|
+
'.sh': 'Shell',
|
|
49
|
+
'.sql': 'SQL',
|
|
50
|
+
'.sqlfluff': 'SQLFluff',
|
|
51
|
+
'.sqlfluffignore': 'SQLFluffIgnore',
|
|
52
|
+
'.svg': 'SVG',
|
|
53
|
+
'.swift': 'Swift',
|
|
54
|
+
'.tf': 'Terraform',
|
|
55
|
+
'.toml': 'TOML',
|
|
56
|
+
'.ts': 'TypeScript',
|
|
57
|
+
'.tsv': 'TSV',
|
|
58
|
+
'.tsx': 'TSX',
|
|
59
|
+
'.txt': 'Text',
|
|
60
|
+
'.vue': 'Vue',
|
|
61
|
+
'.xml': 'XML',
|
|
62
|
+
'.yaml': 'YAML',
|
|
63
|
+
'.yml': 'YAML',
|
|
64
|
+
# Special cases for files without extensions or .format files
|
|
65
|
+
"codeowners": "CODEOWNERS",
|
|
66
|
+
'dockerfile': 'Dockerfile',
|
|
67
|
+
'makefile': 'Makefile',
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
LANGUAGE_BY_BASENAME = {
|
|
71
|
+
'go.mod': 'go.mod',
|
|
72
|
+
'go.sum': 'go.sum',
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def get_fileext(file_path):
|
|
77
|
+
""" Get the file extension of a file """
|
|
78
|
+
file_type = None
|
|
79
|
+
|
|
80
|
+
#if os.path.isfile(file_path):
|
|
81
|
+
# file_type = os.path.splitext(file_path)[1]
|
|
82
|
+
file_type = os.path.splitext(file_path)[1]
|
|
83
|
+
|
|
84
|
+
if file_type:
|
|
85
|
+
return file_type
|
|
86
|
+
else:
|
|
87
|
+
return os.path.basename(file_path)
|
|
88
|
+
|
|
89
|
+
def get_extension_filetype(file_ext):
|
|
90
|
+
""" Get the file extension of a file, using an exact match """
|
|
91
|
+
|
|
92
|
+
if file_ext:
|
|
93
|
+
return EXT_FILETYPES.get(file_ext.lower(), None)
|
|
94
|
+
else:
|
|
95
|
+
return None
|
|
96
|
+
|
|
97
|
+
def get_filename_metatypes(file_path):
|
|
98
|
+
"""
|
|
99
|
+
Return an array of metatypes based on the file_path
|
|
100
|
+
For example:
|
|
101
|
+
pyproject.toml will return build, dependencies
|
|
102
|
+
.github/workflows/python-app.yml will return Github, workflow
|
|
103
|
+
"""
|
|
104
|
+
|
|
105
|
+
filename = os.path.basename(file_path).lower()
|
|
106
|
+
ext = get_fileext(file_path)
|
|
107
|
+
|
|
108
|
+
file_no_ext = os.path.splitext(filename)[0]
|
|
109
|
+
|
|
110
|
+
tags = []
|
|
111
|
+
|
|
112
|
+
if ext == ".pm":
|
|
113
|
+
# Perl module
|
|
114
|
+
# Languauge should already be set by the extension
|
|
115
|
+
tags.append("module")
|
|
116
|
+
|
|
117
|
+
if filename == "pyproject.toml":
|
|
118
|
+
tags.append("build")
|
|
119
|
+
tags.append("dependencies")
|
|
120
|
+
tags.append("Python")
|
|
121
|
+
|
|
122
|
+
if ".github" in file_path:
|
|
123
|
+
tags.append("Github")
|
|
124
|
+
tags.append("Git")
|
|
125
|
+
|
|
126
|
+
if filename == '.gitattributes':
|
|
127
|
+
tags.append("Git")
|
|
128
|
+
|
|
129
|
+
if filename == "eslint.config.js":
|
|
130
|
+
tags.extend(["JavaScript", "linter", "eslint", "config"])
|
|
131
|
+
|
|
132
|
+
if filename == ".mailmap":
|
|
133
|
+
tags.append("Git")
|
|
134
|
+
|
|
135
|
+
if is_pip_requirements(filename):
|
|
136
|
+
#if filename == "requirements.txt":
|
|
137
|
+
tags.append("pip")
|
|
138
|
+
tags.append("Python")
|
|
139
|
+
tags.append("dependencies")
|
|
140
|
+
|
|
141
|
+
if filename == '.sqlfluff':
|
|
142
|
+
tags.append("SQLFluff")
|
|
143
|
+
tags.append("SQL")
|
|
144
|
+
tags.append("linter")
|
|
145
|
+
|
|
146
|
+
if filename == ".mailmap":
|
|
147
|
+
tags.append("Git")
|
|
148
|
+
|
|
149
|
+
if filename == '.nvmrc':
|
|
150
|
+
tags.append("Node")
|
|
151
|
+
tags.append("dependencies")
|
|
152
|
+
|
|
153
|
+
# Usually the filename will be uppercase CODEOWNERS for GitHub
|
|
154
|
+
if filename == "codeowners":
|
|
155
|
+
tags.append("Git")
|
|
156
|
+
|
|
157
|
+
if filename == ".gitignore":
|
|
158
|
+
tags.append("Git")
|
|
159
|
+
tags.append("ignore")
|
|
160
|
+
|
|
161
|
+
if filename == "dockerfile":
|
|
162
|
+
tags.append("IaC")
|
|
163
|
+
tags.append("Docker")
|
|
164
|
+
tags.append("dependencies")
|
|
165
|
+
|
|
166
|
+
if filename == ".dockerignore":
|
|
167
|
+
tags.append("Docker")
|
|
168
|
+
tags.append("ignore")
|
|
169
|
+
|
|
170
|
+
if filename == "makefile":
|
|
171
|
+
tags.append("build")
|
|
172
|
+
|
|
173
|
+
if ".github/workflows" in file_path:
|
|
174
|
+
tags.append("workflow")
|
|
175
|
+
|
|
176
|
+
if filename == "go.mod":
|
|
177
|
+
tags.append("Go")
|
|
178
|
+
tags.append("module")
|
|
179
|
+
tags.append("dependencies")
|
|
180
|
+
|
|
181
|
+
if filename == "go.sum":
|
|
182
|
+
tags.append("Go")
|
|
183
|
+
tags.append("dependencies")
|
|
184
|
+
# TODO - check what we actually want to call hashes etc
|
|
185
|
+
# maybe integrity-checks
|
|
186
|
+
tags.append("checksum")
|
|
187
|
+
|
|
188
|
+
if filename == ".sqlfluffignore":
|
|
189
|
+
tags.append("SQLFluff")
|
|
190
|
+
tags.append("ignore")
|
|
191
|
+
|
|
192
|
+
if filename == "codefresh.yml":
|
|
193
|
+
tags.append("pipeline")
|
|
194
|
+
tags.append("Codefresh")
|
|
195
|
+
|
|
196
|
+
if filename == ".travis.yml":
|
|
197
|
+
tags.append("pipeline")
|
|
198
|
+
tags.append("TravisCI")
|
|
199
|
+
|
|
200
|
+
if filename in ( "package.json", "package-lock.json"):
|
|
201
|
+
tags.append("npm")
|
|
202
|
+
tags.append("dependencies")
|
|
203
|
+
|
|
204
|
+
if filename == "pom.xml":
|
|
205
|
+
tags.append("maven")
|
|
206
|
+
tags.append("build")
|
|
207
|
+
tags.append("dependencies")
|
|
208
|
+
|
|
209
|
+
if filename == "build.gradle":
|
|
210
|
+
tags.append("gradle")
|
|
211
|
+
tags.append("build")
|
|
212
|
+
tags.append("dependencies")
|
|
213
|
+
|
|
214
|
+
if file_no_ext == "license":
|
|
215
|
+
tags.append("license")
|
|
216
|
+
|
|
217
|
+
return tags
|
|
218
|
+
|
|
219
|
+
def check_shebang(file_path):
|
|
220
|
+
""" Check if a file has a shebang """
|
|
221
|
+
try:
|
|
222
|
+
|
|
223
|
+
with open(file_path) as file:
|
|
224
|
+
first_line = file.readline()
|
|
225
|
+
if first_line and first_line.startswith("#!"):
|
|
226
|
+
return first_line.strip()
|
|
227
|
+
else:
|
|
228
|
+
return None
|
|
229
|
+
|
|
230
|
+
except FileNotFoundError:
|
|
231
|
+
# TODO - better logging instead of print
|
|
232
|
+
#print(f"File {file_path} not found")
|
|
233
|
+
return None
|
|
234
|
+
except UnicodeDecodeError:
|
|
235
|
+
# TODO - log this exception
|
|
236
|
+
return None
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def get_shebang_language(shebang):
|
|
240
|
+
""" Return the language of a shebang """
|
|
241
|
+
lang = extract_shebang_language(shebang)
|
|
242
|
+
return lang
|
|
243
|
+
|
|
244
|
+
def load_gitignore_patterns(directory):
|
|
245
|
+
"""
|
|
246
|
+
Load gitignore patterns from a directory
|
|
247
|
+
"""
|
|
248
|
+
gitignore_path = os.path.join(directory, '.gitignore')
|
|
249
|
+
|
|
250
|
+
if os.path.exists(gitignore_path):
|
|
251
|
+
|
|
252
|
+
with open(gitignore_path, 'r', encoding='utf-8') as file:
|
|
253
|
+
patterns = file.read().splitlines()
|
|
254
|
+
# Add .git to the patterns
|
|
255
|
+
patterns.append('.git')
|
|
256
|
+
# These seem to be ignore by git
|
|
257
|
+
patterns.append('.jekyll-cache')
|
|
258
|
+
patterns.append('.ruff_cache')
|
|
259
|
+
patterns.append('.DS_Store')
|
|
260
|
+
|
|
261
|
+
return pathspec.PathSpec.from_lines('gitwildmatch', patterns)
|
|
262
|
+
return None
|
|
263
|
+
|
|
264
|
+
def identify_files(directory):
|
|
265
|
+
"""
|
|
266
|
+
Identify files in a directory.
|
|
267
|
+
Returns a dict of the relative path filenams to their file_type
|
|
268
|
+
"""
|
|
269
|
+
|
|
270
|
+
gitignore_spec = load_gitignore_patterns(directory)
|
|
271
|
+
|
|
272
|
+
file_paths = {}
|
|
273
|
+
|
|
274
|
+
for root, _, files in os.walk(directory):
|
|
275
|
+
for file in files:
|
|
276
|
+
full_path = os.path.join(root, file)
|
|
277
|
+
|
|
278
|
+
relative_path = os.path.relpath(full_path, directory)
|
|
279
|
+
if gitignore_spec and gitignore_spec.match_file(relative_path):
|
|
280
|
+
continue
|
|
281
|
+
|
|
282
|
+
ftype = get_language(full_path)
|
|
283
|
+
|
|
284
|
+
if directory == ".":
|
|
285
|
+
full_path = full_path.removeprefix("./")
|
|
286
|
+
|
|
287
|
+
# TODO - see if we want to add a parameter to return the full path
|
|
288
|
+
#file_paths[fulle_path] = ftype
|
|
289
|
+
file_paths[relative_path] = ftype
|
|
290
|
+
|
|
291
|
+
return file_paths
|
|
292
|
+
|
|
293
|
+
def find_files(directory,all_files=None):
|
|
294
|
+
"""
|
|
295
|
+
Find all files in a directory, honoring the gitignore patterns.
|
|
296
|
+
If all_files = True, then find everything.
|
|
297
|
+
Returns a list of the relative path filenames
|
|
298
|
+
"""
|
|
299
|
+
|
|
300
|
+
gitignore_spec = load_gitignore_patterns(directory)
|
|
301
|
+
if all_files:
|
|
302
|
+
gitignore_spec = None
|
|
303
|
+
|
|
304
|
+
file_paths = []
|
|
305
|
+
|
|
306
|
+
for root, _, files in os.walk(directory):
|
|
307
|
+
for file in files:
|
|
308
|
+
full_path = os.path.join(root, file)
|
|
309
|
+
|
|
310
|
+
relative_path = os.path.relpath(full_path, directory)
|
|
311
|
+
if gitignore_spec and gitignore_spec.match_file(relative_path):
|
|
312
|
+
# Ignore and continue
|
|
313
|
+
continue
|
|
314
|
+
|
|
315
|
+
if directory == ".":
|
|
316
|
+
full_path = full_path.removeprefix("./")
|
|
317
|
+
|
|
318
|
+
file_paths.append(relative_path)
|
|
319
|
+
|
|
320
|
+
return file_paths
|
|
321
|
+
|
|
322
|
+
def extract_shebang_language(shebang: str) -> str:
|
|
323
|
+
"""
|
|
324
|
+
Take a string like
|
|
325
|
+
#!/usr/bin/env python3
|
|
326
|
+
and return the language (python3)
|
|
327
|
+
"""
|
|
328
|
+
parts = shebang.split()
|
|
329
|
+
# Check if the shebang string starts with '#!' and has at least two parts
|
|
330
|
+
# Check for #!/usr/bin/env python3 pattern
|
|
331
|
+
if "#!" in parts[0] and len(parts) > 1:
|
|
332
|
+
|
|
333
|
+
# Split the second part by '/' and check if it contains 'env'
|
|
334
|
+
if 'env' in parts[0]:
|
|
335
|
+
interpreter = parts[-1]
|
|
336
|
+
#return parts[-1] # The interpreter is the last part
|
|
337
|
+
if interpreter.startswith("python"):
|
|
338
|
+
return "Python"
|
|
339
|
+
else:
|
|
340
|
+
return interpreter
|
|
341
|
+
|
|
342
|
+
# Check for the following pattern
|
|
343
|
+
# #!/usr/local/bin/perl style pattern
|
|
344
|
+
else:
|
|
345
|
+
return shebang.split('/')[-1] # Otherwise, the interpreter is the last
|
|
346
|
+
|
|
347
|
+
return None
|
|
348
|
+
|
|
349
|
+
def get_language_edge_cases(file_path):
|
|
350
|
+
"""
|
|
351
|
+
Handle edge cases where certain filenames are special file types
|
|
352
|
+
Seems to be a go thing ...
|
|
353
|
+
"""
|
|
354
|
+
basename = os.path.basename(file_path)
|
|
355
|
+
|
|
356
|
+
if basename:
|
|
357
|
+
return LANGUAGE_BY_BASENAME.get(basename.lower())
|
|
358
|
+
else:
|
|
359
|
+
return None
|
|
360
|
+
|
|
361
|
+
def get_language(file_path):
|
|
362
|
+
""" Return the language of a file """
|
|
363
|
+
ext = get_fileext(file_path)
|
|
364
|
+
lang = None
|
|
365
|
+
|
|
366
|
+
lang_by_basename = get_language_edge_cases(file_path)
|
|
367
|
+
if lang_by_basename:
|
|
368
|
+
return lang_by_basename
|
|
369
|
+
|
|
370
|
+
if ext:
|
|
371
|
+
lang = get_extension_filetype(ext)
|
|
372
|
+
|
|
373
|
+
if lang:
|
|
374
|
+
return lang
|
|
375
|
+
|
|
376
|
+
shebang = check_shebang(file_path)
|
|
377
|
+
|
|
378
|
+
if shebang:
|
|
379
|
+
lang = get_shebang_language(shebang)
|
|
380
|
+
|
|
381
|
+
return lang
|
|
382
|
+
|
|
383
|
+
#def basename_check(file_path):
|
|
384
|
+
# """
|
|
385
|
+
# Return a guessed type based on the basename
|
|
386
|
+
# """
|
|
387
|
+
|
|
388
|
+
# return None
|
|
389
|
+
|
|
390
|
+
def extract_urls(text):
|
|
391
|
+
"""
|
|
392
|
+
Find all HTTP/S URLs from a given string and return a list of URLs found.
|
|
393
|
+
|
|
394
|
+
"""
|
|
395
|
+
url_pattern = re.compile(r'https?://[^\s\"\'\)]+')
|
|
396
|
+
urls = re.findall(url_pattern, text)
|
|
397
|
+
return urls
|
|
398
|
+
|
|
399
|
+
def extract_urls_from_file(file_path):
|
|
400
|
+
"""
|
|
401
|
+
Extract URLs from a given file.
|
|
402
|
+
|
|
403
|
+
Args:
|
|
404
|
+
file_path (str): File to extract URLs from
|
|
405
|
+
|
|
406
|
+
Returns:
|
|
407
|
+
list: List of URLs found in the file
|
|
408
|
+
|
|
409
|
+
Raises:
|
|
410
|
+
FileNotFoundError: If the specified file does not exist
|
|
411
|
+
UnicodeDecodeError: If the file cannot be decoded as UTF-8
|
|
412
|
+
"""
|
|
413
|
+
if not os.path.exists(file_path):
|
|
414
|
+
raise FileNotFoundError(f"File not found: {file_path}")
|
|
415
|
+
|
|
416
|
+
try:
|
|
417
|
+
with open(file_path, 'r', encoding='utf-8') as file:
|
|
418
|
+
content = file.read()
|
|
419
|
+
return extract_urls(content)
|
|
420
|
+
except UnicodeDecodeError as e:
|
|
421
|
+
raise UnicodeDecodeError(f"Unable to decode file {file_path} as UTF-8: {str(e)}")
|
|
422
|
+
|
|
423
|
+
def is_pip_requirements(filename: str) -> bool:
|
|
424
|
+
"""
|
|
425
|
+
Returns True if `filename` matches typical pip requirements filenames.
|
|
426
|
+
Examples of matching filenames:
|
|
427
|
+
- requirements.txt
|
|
428
|
+
- requirements-dev.txt
|
|
429
|
+
- requirements-dev.in
|
|
430
|
+
- requirements.in
|
|
431
|
+
- requirements.dev.txt
|
|
432
|
+
"""
|
|
433
|
+
|
|
434
|
+
PATTERN = re.compile(r'^requirements([-._a-zA-Z0-9]*)\.(txt|in)$')
|
|
435
|
+
|
|
436
|
+
return bool(PATTERN.match(filename))
|
panopticas_cli.py
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# panopticas.py
|
|
2
|
+
import click
|
|
3
|
+
from prettytable import PrettyTable
|
|
4
|
+
import panopticas as ft
|
|
5
|
+
|
|
6
|
+
VERSION = "0.0.9"
|
|
7
|
+
|
|
8
|
+
@click.group()
|
|
9
|
+
def cli():
|
|
10
|
+
"""Panopticas is a tool for identifying file types, code and git repositories.
|
|
11
|
+
|
|
12
|
+
In future, it will be possible identify external dependencies
|
|
13
|
+
(e.g. URLs, cloud providers)
|
|
14
|
+
|
|
15
|
+
For documentation on how commands run `panopticas COMMAND --help`.
|
|
16
|
+
|
|
17
|
+
See also https://panopticas.io/
|
|
18
|
+
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
@cli.command("assess")
|
|
22
|
+
@click.option('-unknown', is_flag=True, default=False, help="Show only files with an unknown language type.")
|
|
23
|
+
@click.argument('directory', required=False, type=click.Path(exists=True))
|
|
24
|
+
def assess(directory,unknown):
|
|
25
|
+
"""Assess a directory."""
|
|
26
|
+
click.echo()
|
|
27
|
+
if directory:
|
|
28
|
+
click.echo(f'Assessing directory: {directory}')
|
|
29
|
+
else:
|
|
30
|
+
click.echo('Assessing current directory.')
|
|
31
|
+
directory = "."
|
|
32
|
+
files = ft.identify_files(directory)
|
|
33
|
+
click.echo(f'Found {len(files)} files.\n')
|
|
34
|
+
table = PrettyTable()
|
|
35
|
+
table.field_names = ["File", "Language", "Meta"]
|
|
36
|
+
table.align["File"] = "l"
|
|
37
|
+
table.align["Language"] = "l"
|
|
38
|
+
table.align["Meta"] = "l"
|
|
39
|
+
|
|
40
|
+
for file, file_type in files.items():
|
|
41
|
+
meta = ft.get_filename_metatypes(file) if ft.get_filename_metatypes(file) else ""
|
|
42
|
+
if meta:
|
|
43
|
+
meta = ", ".join(meta)
|
|
44
|
+
# with "unknown", we only include files with unknown language types (e.g. None)
|
|
45
|
+
if unknown:
|
|
46
|
+
if file_type is None:
|
|
47
|
+
#print(f"File: {file} is of unknown language type")
|
|
48
|
+
table.add_row([file, file_type, meta])
|
|
49
|
+
else:
|
|
50
|
+
# Default is we add it to the table
|
|
51
|
+
table.add_row([file, file_type, meta])
|
|
52
|
+
|
|
53
|
+
print(table,"\n")
|
|
54
|
+
|
|
55
|
+
@cli.command("file")
|
|
56
|
+
@click.argument('file', required=True,type=click.Path(exists=True))
|
|
57
|
+
def identify(file):
|
|
58
|
+
"""Assess a filetype."""
|
|
59
|
+
click.echo(f'\nAssessing filetype for file {file}')
|
|
60
|
+
click.echo()
|
|
61
|
+
table = PrettyTable()
|
|
62
|
+
table.field_names = ["Method", "Result"]
|
|
63
|
+
table.align["Method"] = "l"
|
|
64
|
+
table.align["Result"] = "l"
|
|
65
|
+
|
|
66
|
+
table.add_row(["File extenion", ft.get_fileext(file)])
|
|
67
|
+
table.add_row(["File type", ft.get_extension_filetype(ft.get_fileext(file))])
|
|
68
|
+
shebang = ft.check_shebang(file)
|
|
69
|
+
table.add_row(["Shebang", shebang])
|
|
70
|
+
table.add_row(["Shebang Language", ft.extract_shebang_language(shebang) if shebang else None])
|
|
71
|
+
table.add_row(["Meta", ft.get_filename_metatypes(file)])
|
|
72
|
+
|
|
73
|
+
urls = []
|
|
74
|
+
|
|
75
|
+
urls = ft.extract_urls_from_file(file)
|
|
76
|
+
table.add_row(["URLs", '\n'.join(urls)])
|
|
77
|
+
|
|
78
|
+
print(table)
|
|
79
|
+
print()
|
|
80
|
+
|
|
81
|
+
@cli.command("urls")
|
|
82
|
+
@click.option('-all-files', is_flag=True, default=False, help="Show all files, no gitignore.")
|
|
83
|
+
@click.argument('directory', required=True, type=click.Path(exists=True))
|
|
84
|
+
def find_urls(directory,all_files):
|
|
85
|
+
"""
|
|
86
|
+
Find and show urls for all files in a given directory.
|
|
87
|
+
"""
|
|
88
|
+
files = ft.find_files(directory,all_files=all_files)
|
|
89
|
+
|
|
90
|
+
table = PrettyTable()
|
|
91
|
+
table.field_names = ["Filename", "URLs"]
|
|
92
|
+
table.align["Filename"] = "l"
|
|
93
|
+
table.align["URLs"] = "l"
|
|
94
|
+
|
|
95
|
+
# ft.find_files(directory)
|
|
96
|
+
for f in files:
|
|
97
|
+
urls = ft.extract_urls_from_file(f)
|
|
98
|
+
table.add_row([f, '\n'.join(urls)])
|
|
99
|
+
|
|
100
|
+
print(table)
|
|
101
|
+
print()
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@cli.command("version")
|
|
105
|
+
def version():
|
|
106
|
+
"""Print the version of Panopticas."""
|
|
107
|
+
click.echo(f"Panopticas CLI version {VERSION}")
|
|
108
|
+
click.echo(f"Panopticas v{ft.VERSION}")
|
|
109
|
+
|
|
110
|
+
if __name__ == '__main__':
|
|
111
|
+
cli()
|