Skip to content

Commit

Permalink
Merge pull request #3 from lordyavin/develop
Browse files Browse the repository at this point in the history
Initial Release
  • Loading branch information
lordyavin authored May 28, 2020
2 parents 586fadf + 3db1e4e commit 10b4cd6
Show file tree
Hide file tree
Showing 10 changed files with 328 additions and 0 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -127,3 +127,4 @@ dmypy.json

# Pyre type checker
.pyre/
/runtime/
17 changes: 17 additions & 0 deletions .project
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>eightanu</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.python.pydev.PyDevBuilder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.python.pydev.pythonNature</nature>
</natures>
</projectDescription>
14 changes: 14 additions & 0 deletions .pydevproject
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<?eclipse-pydev version="1.0"?><pydev_project>

<pydev_pathproperty name="org.python.pydev.PROJECT_SOURCE_PATH">

<path>/${PROJECT_DIR_NAME}/src</path>

</pydev_pathproperty>

<pydev_property name="org.python.pydev.PYTHON_PROJECT_VERSION">python interpreter</pydev_property>

<pydev_property name="org.python.pydev.PYTHON_PROJECT_INTERPRETER">eightanu venv</pydev_property>

</pydev_project>
1 change: 1 addition & 0 deletions .settings/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/org.eclipse.core.resources.prefs
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,8 @@
# eightanu
Python script to export 8a.nu logbook

# Introduction
Since the restart of 8a.nu you can no longer export your ascents. This project aims to provide a tool to export personal ascents.

# Usage
more to come...
2 changes: 2 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
selenium==3.141.0
urllib3==1.25.9
Empty file added src/eightanu/__init__.py
Empty file.
119 changes: 119 additions & 0 deletions src/eightanu/cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
#!/usr/local/bin/python3.6
# encoding: utf-8
'''
eightanu.cli -- The command line interface to export the new 8a.new ascents data.
eightanu.cli is an exporter that scrap your ascents from the new 8a.nu website.
@author: lordyavin
@copyright: 2020 lordyavin. All rights reserved.
@license: MIT
@contact: github@klesatschke.net
@deffield updated: Updated
'''
from argparse import ArgumentParser
from argparse import RawDescriptionHelpFormatter
import os
import sys

from eightanu import webdriver
from eightanu.export import export

__all__ = []
__version__ = 0.1
__date__ = '2020-05-27'
__updated__ = '2020-05-28'

DEBUG = 0
TESTRUN = 0
PROFILE = 0


class CLIError(Exception):
'''Generic exception to raise and log different fatal errors.'''

def __init__(self, msg):
super(CLIError).__init__(type(self))
self.msg = "E: %s" % msg

def __str__(self):
return self.msg

def __unicode__(self):
return self.msg


def main(argv=None): # IGNORE:C0111
'''Command line options.'''

if argv is None:
argv = sys.argv
else:
sys.argv.extend(argv)

program_name = os.path.basename(sys.argv[0])
program_version = "v%s" % __version__
program_build_date = str(__updated__)
program_version_message = '%%(prog)s %s (%s)' % (program_version, program_build_date)
program_shortdesc = __import__('__main__').__doc__.split("\n")[1]
program_license = '''%s
Created by user_name on %s.
Copyright 2020 organization_name. All rights reserved.
Licensed under the Apache License 2.0
http://www.apache.org/licenses/LICENSE-2.0
Distributed on an "AS IS" basis without warranties
or conditions of any kind, either express or implied.
USAGE
''' % (program_shortdesc, str(__date__))

try:
# Setup argument parser
parser = ArgumentParser(description=program_license, formatter_class=RawDescriptionHelpFormatter)
parser.add_argument("-b", "--browser", dest="browser", type=str, choices=webdriver.SUPPORTED_BROWSER, required=True)
parser.add_argument("-v", "--verbose", dest="verbose", action="count", help="set verbosity level [default: %(default)s]", default=0)
parser.add_argument('-V', '--version', action='version', version=program_version_message)

parser.add_argument("username", type=str, help="Your 8a user name")

# Process arguments
args = parser.parse_args()

verbose = args.verbose
if verbose > 0:
print("Verbose mode on")

export(args.browser, args.username, verbose)

return 0
except KeyboardInterrupt:
### handle keyboard interrupt ###
return 0
except Exception as e:
if DEBUG or TESTRUN:
raise e
indent = len(program_name) * " "
sys.stderr.write(program_name + ": " + repr(e) + "\n")
sys.stderr.write(indent + " for help use --help")
return 2


if __name__ == "__main__":
if TESTRUN:
import doctest
doctest.testmod()
if PROFILE:
import cProfile
import pstats
profile_filename = 'eightanu.export_ascents_profile.txt'
cProfile.run('main()', profile_filename)
statsfile = open("profile_stats.txt", "wb")
p = pstats.Stats(profile_filename, stream=statsfile)
stats = p.strip_dirs().sort_stats('cumulative')
stats.print_stats()
statsfile.close()
sys.exit(0)
sys.exit(main())
95 changes: 95 additions & 0 deletions src/eightanu/export.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
'''
eightanu.export -- The 8a.nu logbook export
@author: lordyavin
@copyright: 2020 lordyavin. All rights reserved.
@license: MIT
@contact: github@klesatschke.net
@deffield updated: Updated
'''
from selenium.webdriver.remote.webelement import WebElement

from eightanu import webdriver
from selenium.webdriver.remote.webdriver import WebDriver
from eightanu.webdriver import SUPPORTED_BROWSER

def _select_alltime_ascents(driver):
assert isinstance(driver, WebDriver)

filters = driver.find_element_by_class_name("ascent-filters")
assert isinstance(filters, WebElement)

all_options = filters.find_elements_by_tag_name("option")
for option in all_options:
if option.text == "All Time":
option.click()

def _sort_by_date(driver):
assert isinstance(driver, WebDriver)

order = driver.find_element_by_class_name("ascent-filters__order")
all_options = order.find_elements_by_tag_name("option")
for option in all_options:
if option.text == "Date":
option.click()


def _read_table_headers(table):
assert isinstance(table, WebElement)

thead = table.find_element_by_tag_name("thead")
table_headers = thead.find_elements_by_tag_name("th")[1:] # skip first empty cell
headers = ["DATE"] + [cell.text.strip() for cell in table_headers]
return headers


def _read_ascents(table, headers):
assert isinstance(table, WebElement)
assert isinstance(headers, (list, tuple))

ascents = []
name_idx = headers.index("NAME")
groups = table.find_elements_by_tag_name("tbody")
for group in groups:
date = group.find_element_by_tag_name("th").text
rows = group.find_elements_by_tag_name("tr")
for row in rows:
cells = row.find_elements_by_tag_name("td")[1:] # skip first empty cell
if cells:
ascent = [date] + [cell.text.strip() for cell in cells]
name = ascent[name_idx]
if "\n" in name:
ascent[name_idx] = name[:name.index("\n")]
ascents.append(ascent)

return ascents


def _print_logbook(headers, ascents):
assert isinstance(headers, (list, tuple))
assert isinstance(ascents, (list, tuple))

print(";".join(headers))
for ascent in ascents:
print(";".join(ascent))

def export(browser, username, verbose=0):
assert browser in SUPPORTED_BROWSER
assert isinstance(username, str)
assert isinstance(verbose, int)

username = username.replace(" ", "-").lower()
url = "https://www.8a.nu/user/{username}/sportclimbing".format(username=username)

driver = webdriver.get(browser, verbose)
try:
driver.get(url)
_select_alltime_ascents(driver)
_sort_by_date(driver)

table = driver.find_element_by_class_name("user-ascents")
headers = _read_table_headers(table)
ascents = _read_ascents(table, headers)
_print_logbook(headers, ascents)
finally:
driver.close()
73 changes: 73 additions & 0 deletions src/eightanu/webdriver.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# encoding: utf-8
'''
eightanu.webdriver -- Handles the selenium webdrivers.
@author: lordyavin
@copyright: 2020 lordyavin. All rights reserved.
@license: MIT
@contact: github@klesatschke.net
@deffield updated: Updated
'''
import os
from zipfile import ZipFile

from selenium import webdriver
from selenium.common.exceptions import WebDriverException
import urllib3
from urllib3.response import HTTPResponse

FIREFOX_DRIVER_URL = "https://github.com/mozilla/geckodriver/releases/download/v0.26.0/geckodriver-v0.26.0-win64.zip"
CHROME_DRIVER_URL = "https://chromedriver.storage.googleapis.com/83.0.4103.39/chromedriver_win32.zip"

FIREFOX = "Firefox"
CHROME = "Chrome"

SUPPORTED_BROWSER = (
FIREFOX,
CHROME,
)

DOWNLOAD = {
FIREFOX: FIREFOX_DRIVER_URL,
CHROME: CHROME_DRIVER_URL
}

DRIVER = {
FIREFOX: webdriver.Firefox,
CHROME: webdriver.Chrome,
}


def download(url, verbose=0):
http = urllib3.PoolManager()

if verbose:
print("Downloading %s" % url)

response = http.request('GET', url)
assert isinstance(response, HTTPResponse)
zipfilename = os.path.basename(url)

with open(zipfilename, "wb") as zipfile:
zipfile.write(response.data)

if verbose:
print("Extracting %s" % zipfilename)

with ZipFile(zipfilename) as zipfile:
zipfile.extractall(".")

os.remove(zipfilename)


def get(browser, verbose=0):
assert browser in SUPPORTED_BROWSER
try:
Driver = DRIVER[browser]
driver = Driver()
except WebDriverException as e:
if verbose:
print(e.msg)
download(DOWNLOAD[browser], verbose)
driver = Driver()
return driver

0 comments on commit 10b4cd6

Please sign in to comment.