This commit is contained in:
kendfss 2021-06-19 02:52:02 +01:00
commit 8128d4bb1d
8 changed files with 516 additions and 0 deletions

140
.gitignore vendored Normal file
View File

@ -0,0 +1,140 @@
*.json
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/

98
readme.md Normal file
View File

@ -0,0 +1,98 @@
# serva
[]()
Start a python server with the ability to copy your local address to clipboard or send it via email, set a custom port, and/or automatically open in browser of choice.
**why?** because you're worth it
[**how?**](#usage)
### Usage
*Note:* CLI will always defer to execute "root/serve.ext" if the root directory contains a file named "serve.ext"
*Note:* If/where used, the dot is an optional argument
##### Set preferred browser
```shell
> serve setting -o "path/or/alias/to/your/favourite/browser"
> serve setting --open "path/or/alias/to/your/favourite/browser"
```
*Note:* If unset, the command will open the address with the system default for the chosen protocol
*Note:* Once set, the command will open the address with this browser by default
##### Set protocol[^1]
```shell
> serve setting -p "name or prefix of chosen protocol"
> serve setting --protocol "name or prefix of chosen protocol"
```
*Note:* Recognized protocols: http, gopher, gemini
##### Add script extension[^1][^4]
```shell
> serve setting -s ".ext"
> serve setting --script ".ext"
```
*Note:* Recognized protocols: http, gopher, gemini
##### Suppress browser [^2]
```shell
> serve running -d
> serve running --dont
```
<!-- ##### Enable blocking
```shell
> serve -b
> serve --block
``` -->
##### Non-current directory[^5]
```shell
> serve running -r "path/to/project/folder"
> serve running --root "path/to/project/folder"
```
##### Copy to clipboard[^2]
```shell
> serve running -c
> serve running --copy
```
##### Send email[^3]
```shell
> serve running -m "email.address@sending.to"
> serve running --mail "email.address@sending.to"
```
*Note:* Will not work unless a source email has been set in settings. You can use the same flags for setting[^3]
### Supported Platforms
- [x] Windows
- [ ] Linux
- [ ] Mac
### Supported protocols
- [x] Gemini
- [x] Gopher
- [x] HTML
### Supported Email Services
- [x] Gmail[^6]
- [ ] Proton
- [ ] Hotmail
- [ ] Yahoo
- [ ] Yandex
### Recognized Scripts
If the targeted directory contains a file named "serve" with any of the following extensions, it will be run automatically
- [x] js
- [x] py
- [x] sh
- [x] ps1
- [x] bat
### Notes
[1^]: Will propmt for further info.
[2^]: Toggles saved value if used as setting.
[3^]: Accepts argument if used as setting.
[4^]: Only implemented in setting mode.
[5^]: Only implemented in running mode.
[6^]: You must enable 'unsafe' mode (in order to send emails programmatically) manually.
[6^]:

1
requirements.txt Normal file
View File

@ -0,0 +1 @@
pyperclip

15
serva/__init__.py Normal file
View File

@ -0,0 +1,15 @@
if __name__ == '__main__':
pass

157
serva/cli.py Normal file
View File

@ -0,0 +1,157 @@
import subprocess, os, argparse, json, re
import pyperclip
from .mail import quick_mail
from .ip import get_ip
here, this = os.path.split(__file__)
settings_path = os.path.join(here, 'settings.json')
class UnrecognizedProtocolError(Exception): pass
def load_settings():
with open(settings_path, 'r') as fob:
return json.load(fob)
def save_settings():
with open(settings_path, 'w') as fob:
json.dump(settings, fob, sort_keys=True)
settings = {
"protocols": {
"gemini": "gemini://",
"gopher": "gopher://",
"http": "http://",
},
"email": {
"address": None,
"password": None
},
# "block": False,
"browser": None,
"number": 8000,
"dont": False,
"copy": False,
"extensions": {
".ps1": '',
".sh": '',
".bat": '',
".py": 'python',
".js": 'node',
}
} if not os.path.exists(settings_path) else load_settings()
save_settings()
def setting(args):
if args.protocol:
settings['protocols'][args.protocol] = input(f"Enter a uri prefix for the {args.protocol} protocol:\n\t")
yield f'added {protocol} protocol'
if args.mail:
settings['email']['address'] = args.mail
yield f"email address set to {settings['email']['address']}"
settings['email']['password'] = input(f"Enter the password for {settings['email']['address']}:\n\t")
yield f"email password set to {''.join('*' for i in settings['email']['password'])}"
if args.browser:
settings['browser'] = args.browser
yield f"browser set to {settings['browser']}"
# if args.block:
# settings['block'] = not settings['block']
# yield f"block set to {settings['block']}"
if args.number:
settings['number'] = int(args.number)
yield f"number set to {settings['number']}"
if args.dont:
settings['dont'] = not settings['dont']
yield f"dont set to {settings['dont']}"
if args.copy:
settings['copy'] = not settings['copy']
yield f"copy set to {settings['copy']}"
if args.extension:
settings['extensions'][args.script] = input("Which program should execute the script? (leave blank for system default, otherwise include flags as needed)\n\t")
save_settings()
def parse_protocol(protocol):
if protocol.endswith("://"):
return protocol
if (protocol := settings['protocols'].get(protocol)):
return protocol
raise UnrecognizedProtocolError
def find_script():
path = os.getcwd()
fnames = os.listdir(os.getcwd())
for name in fnames:
if os.path.isfile(name):
fmt = "^%s(?P<ext>{})$" % (re.escape("serve."))
patstr = "|".join(fmt.format(ext[1:]) for ext in settings['extensions'].keys())
pat = re.compile(pat, re.I)
# if any(fi)
if (match := pat.match(name)):
return name
def running(args):
os.chdir(args.root)
if (ext := find_script()):
subprocess.run([settings[ext], "serve."+ext])
else:
address = f"{parse_protocol(args.protocol)}{get_ip()}:{args.number}"
if not args.dont:
if (browser := args.open):
subprocess.run([browser, address])
yield f"opened with {browser}"
elif (browser := settings["browser"]):
subprocess.run([browser, address])
yield f"opened with {browser}"
else:
os.startfile(address)
yield "opened with system default"
if args.copy:
pyperclip.copy(address)
if args.mail:
quick_mail(
address,
args.mail,
settings['email']['address'],
settings['email']['password']
)
yield f"serving @ {address}"
subprocess.run(['python', '-m', 'http.server', address, '&' if not args.block else ''])
def handler(args):
if args.mode == "running":
[*map(print, running(args))]
elif args.mode == "setting":
[*map(print, setting(args))]
def main():
parser = argparse.ArgumentParser(description="Serve the current/given directory on a given port. Unless otherwise stated, all arguments are supported in both 'setting' and 'running' modes")
parser.add_argument('mode', default='running', choices="running setting".split(), help="Execute cli in running or setting mode")
parser.add_argument('--root', '-r', default='.', help="root to the directory you wish to serve")
parser.add_argument('--mail', '-m', default=None, help="send local address to an email-address of your choice")
parser.add_argument('--copy', '-c', default=settings['copy'], action='store_true', help="copy url to clipboard")
parser.add_argument('--block', '-b', default=False, action='store_true', help="(unimplemented) Block/unblock the prompt while the server is running")
parser.add_argument('--protocol', '-p', default="http://", help="Set the protocol for the local address")
parser.add_argument('--number', '-n', default=8000, type=int, help="Set the number of the serving port")
parser.add_argument('--dont', '-d', default=settings['dont'], action='store_true', help="Choose not to open in browser")
parser.add_argument('--open', '-o', default=settings['browser'], help="Choose a browser to open in")
parser.add_argument('--extension', '-e', default=None, help="(setting only) lookout for a script named 'serve' with given extension")
handler(parser.parse_args())
if __name__=='__main__':
main()
"""
more info
https://stackoverflow.com/questions/5663787/upload-folders-from-local-system-to-ftp-using-python-script
https://stackoverflow.com/questions/9382045/send-a-file-through-sockets-in-python
https://stackabuse.com/serving-files-with-pythons-simplehttpserver-module/
https://docs.python.org/3.7/library/http.server.html
"""

16
serva/ip.py Normal file
View File

@ -0,0 +1,16 @@
import subprocess
import pyperclip
def get_ip(version:str='ipv4', copy:bool=False) -> str:
"""
Returns the IP address of the device on which the function is called
Dependencies: subprocess.check_result, pyperclip.copy
Arguments: copy=False
Output: ipAddress [str]
"""
call = str(subprocess.check_output('ipconfig')).split('\\n')
line = [l for l in call if version in l.lower()][0]
address = line.strip().strip('. ').strip('\\r').split(': ')[1]
if copy: pyperclip.copy(address)
return address

57
serva/mail.py Normal file
View File

@ -0,0 +1,57 @@
import smtplib, os
smtp_info = {
"gmail": {
"host": "smtp.gmail.com",
"port": 465,
},
}
def quick_mail(
msg:str,
receiver:str,
sender:str,
password:str,
subj:str="IP Address",
head:str="IP Address",
extra:str=os.path.split(os.path.expanduser('~'))[1],
att:str=None
) -> None:
"""
Sends a short email to the given receiver using gmail's smtp configuration with default sender/receiver info taken from user environment variables
Dependencies: os.environ.get [func], smtplib [mod], email.message.EmailMessage [obj]
Arguments: message, **destination
Output: None
"""
message = EmailMessage()
message['Subject'] = f'{subj if subj!=None else head if head!=None else ""}'
message['From'] = sender
message['To'] = receiver
message.set_content(msg)
if any([subj!=None,head!=None,extra!=None]):
message.add_alternative(f"""\
<!DOCTYPE html>
<html style='font-family:courier new;'>
<body>
<h1 style="color:SlateGray;">{head if head!=None else ''}</h1>
<p>{msg}</p>
<P>from {extra if extra else ''}
</body>
</html>
""", subtype='html')
if att != None:
for file in att.split('*'):
with open(file, "rb") as f:
fileData = f.read()
fileName = f.name.split(os.sep)[-1]
fileType = (os.path.splitext(f.name)[1]).replace(".","")
message.add_attachment(fileData, maintype="image", subtype=fileType, filename=fileName)
# with smtplib.SMTP_SSL('smtp.gmail.com', 465) as smtp:
target = sender.split('@')[1].split('.')[0]
with smtplib.SMTP_SSL(
smtp_info[target]['host'],
smtp_info[target]['port'],
) as smtp:
smtp.login(sender, password)
smtp.send_message(message)

32
setup.py Normal file
View File

@ -0,0 +1,32 @@
from setuptools import setup, find_packages
with open('readme.md', 'r') as fob:
long_description = fob.read()
with open('requirements.txt', 'r') as fob:
requirements = fob.readlines()
setup(
name='serva',
version='0.0.1',
author='Kenneth Sabalo',
author_email='kennethsantanasablo@gmail.com',
url='https://github.com/kendfss/serva',
description="CLI for serving web projects locally",
packages=find_packages(),
classifiers=[
"Programming Language :: Python :: 3",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
],
long_description=long_description,
long_description_content_type='text/markdown',
keywords='utilities operating path file system local server web',
license='GNU GPLv3',
requires=requirements,
entry_points={
'console_scripts': [
'serva = serva.cli:main'
]
},
python_requires='>3.8',
)