Skip to content

Instantly share code, notes, and snippets.

@Gabryxx7
Last active May 31, 2024 07:06
Show Gist options
  • Save Gabryxx7/3e7fff8006c5e3d7f2a6ff4031afd34b to your computer and use it in GitHub Desktop.
Save Gabryxx7/3e7fff8006c5e3d7f2a6ff4031afd34b to your computer and use it in GitHub Desktop.
Automatic refresh and booking editing for Bupa Visa Medical Services

Instructions

Set up Selenium's Google Chrome driver

  1. Check your Google Chrome version (Three dots at the top right -> Settings -> From the sidebar on the left go to "About Chrome" -> Annotate your Google Chrome version)
  2. Go to https://chromedriver.chromium.org/downloads
  3. Download the right file for your OS and your Google Chrome Version
  4. Extract it and place it anywhere you'd like, annotate the path to the extracted file

Set up your python environment

I tested this on a fresh environment with Anaconda3 and Python 3.10.2 but it should work without Anaconda too!

Using pip:

pip install -r requirements.txt

Using conda

conda env create --file environment.yml -n test_web conda activate test_web

Install Pushbullet

pip install pushbullet.py==0.9.1
pip install pywebio 

Edit config.yml

  1. Fill in your details (HAP ID, name, surname, date of birth)
  2. Edit the chrome_driver_path the file path you just extracted (including the extracted filename e.g "chromedriver.exe")
  3. (Optional) Edit your date preferences:
    • Add a date range for your appointment
    • Add a list of excluded dates
    • Add a list of excluded weekdays

Set up PushBullet notifications

  1. Go to Pushbullet.com
  2. Create an account
  3. Install the Pushbullet app on your phone and login
  4. Go to your Pushbullet's account's settings (https://www.pushbullet.com/#settings)
  5. Create an access Token and copy it
  6. Add it to config.yml
  7. You'll now receive notifications on your devices through Pushbullet!

Run it!

Once all the dependencies are installed (and the conda environment is activated) just run it with python .\bupaRefresh.py

Let it run for a while, last time it took me about 2 hours to move the appointment from 3 months away to the day after!

import os
import traceback
import selenium
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support.expected_conditions import presence_of_element_located, element_to_be_clickable
from selenium.common.exceptions import ElementClickInterceptedException
import time
from PIL import Image
import io
import requests
import chromedriver_binary
import chromedriver_autoinstaller
import datetime
import oyaml as yaml
import calendar
from pywebio.input import *
from pywebio.output import *
from pywebio.session import *
logs_dir = './logs'
class bcolors:
HEADER = '\033[95m'
OKBLUE = '\033[94m'
OKCYAN = '\033[96m'
OKGREEN = '\033[92m'
WARNING = '\033[93m'
FAIL = '\033[91m'
ENDC = '\033[0m'
BOLD = '\033[1m'
UNDERLINE = '\033[4m'
# Remember to install the chromedriver for your OS, download the version that matches your installed Google Chrome version
# https://chromedriver.chromium.org/downloads
# Global Variables
driver = None
wait = WebDriverWait(driver, 20)
config = None
info = None
log_file = None
telegram_bot_url = None
def notify_telegram(message):
global telegram_bot_url
message = f"BUPA bot: {message}"
try:
requests.get(f"{telegram_bot_url}{message}")
except Exception as ee:
print("Error notifying telegram bot")
"""
Just setting up Selenium with the driver
"""
def read_config():
global config
global info
global telegram_bot_url
config = yaml.full_load(open("config.yml"))
try:
info = yaml.full_load(open("info.no-commit.yml"))
except:
info = yaml.full_load(open("info.yml"))
if info['telegram_token']:
telegram_bot_url = f"https://api.telegram.org/bot{info['telegram_token']}/sendMessage?chat_id={info['chat_id']}&text="
def query(key):
q = config['selectors'][key]
return (q['by'], q['query'])
def init_chrome_driver():
global driver
global wait
global options
options = Options()
options.headless = False # When True it will show a chrome window so you can see what's happening
# options.add_argument('--headless') # ensure GUI is off
options.add_argument("--window-size=400, 300")
options.add_argument('--no-sandbox')
options.add_argument('--disable-dev-shm-usage')
# Initialize the Chrome Driver with the options we set up
# driver = webdriver.Chrome(config["chrome_driver_path"], options=options)
driver = webdriver.Chrome(options=options)
chromedriver_autoinstaller.install()
# This defines a default wait object, the waiting time is 10 seconds
# Whenever you call a wait, it will wait until the even you specify happens
# If it does not happen in the time frame you passed, it will throw a TimeOutException
wait = WebDriverWait(driver, config["wait_element_time"])
def current_time_str():
return datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
def log_print(log_type="i", txt="", with_time=True, end="\n", start="\n", tg_notif=False, to_file=True):
global log_file
global telegram
prefix = ""
if with_time:
prefix = f"{current_time_str()}\t"
color = ""
log_type = log_type.lower()
if log_type in ["i","info","nfo","log"]:
color = bcolors.OKCYAN
elif log_type in ["w","f"]:
color = bcolors.WARNING
elif log_type in ["e","f", "n", "error", "fail", "no"]:
color = bcolors.FAIL
elif log_type in ["s","success", "ok"]:
color = bcolors.OKGREEN
print(start+prefix+color+txt, end=end)
if to_file and log_file is not None:
try:
log_file.write(start+prefix+txt+end+"\n")
log_file.flush()
except Exception as e:
pass
if tg_notif:
notify_telegram(f"{prefix}\n{txt}")
def wait_print(seconds, waiting_msg="", end_msg="", log_type="i", tg_notif=False):
while seconds > 0:
log_print(log_type, f"{waiting_msg} {seconds}s... ", end = "\r", start="", tg_notif=tg_notif, to_file=False)
time.sleep(1)
seconds = seconds - 1
log_print(log_type, f"{waiting_msg} {seconds}s... {end_msg}", end = "\n", start="", tg_notif=tg_notif, to_file=True)
def check_availability():
# Check all available days from the first one
available_days = driver.find_elements(*query('available_days'))
total_available = len(available_days)
with open('dates_log.csv', 'w') as f:
for i in range(0, total_available):
d = available_days[i]
new_date = datetime.datetime.strptime(d.get_attribute("data-value"), '%d/%m/%Y')
if not check_date(new_date, config['app_date']):
continue
f.write(f"{current_time_str()},{new_date}\n")
f.flush()
d.click() # Click on the day
wait.until(element_to_be_clickable(query('available_days_container'))) # Waiting until the page has reloaded
if book_appointment(driver, config, new_date):
return True
available_days = driver.find_elements(*query('available_days')) # Need to get the reference to the new available days
return False
def get_current_booking():
# Get current appointment date
try:
app_date = driver.find_element(*query('current_appointment')).text
config['app_date'] = datetime.datetime.strptime(app_date, '%A, %d %B %Y @ %I:%M %p') # 25 June 2024 @ 10:45 AM
log_print("e", f"Current appointment date: {config['app_date']}", end="")
except Exception as e:
log_print("e", f"Error getting current appointment date: {e}")
def edit_appointment():
# Gotta wait until the "modify/edit date and location" button is visible
edit_btn = wait.until(element_to_be_clickable(query('edit_btn'))) # (By.ID, "ContentPlaceHolder1_repAppointments_lnkChangeAppointment_0")
edit_btn.click()
# Clicking next on the location selection page
next_btn = wait.until(element_to_be_clickable(query('next_btn'))) # (By.ID, "ContentPlaceHolder1_btnCont")
next_btn.click()
def loop_refresh():
logged_in = False
# Loop forever to keep finding closer and closer appointments
while True:
try:
if not logged_in:
login()
edit_appointment()
wait.until(element_to_be_clickable(query('available_days_container')))
except Exception as e:
log_print("e", f"Error navigating to edit booking page{e}", end="")
traceback.print_exception(e)
logged_in = False
time.sleep(2) # take a pause
continue
get_current_booking()
# If this is the first time, we just logged in so this will still be false
if not logged_in:
notify_telegram(f"Current booking date: {config['app_date']}")
logged_in = True
found_available = False
while not found_available:
found_available = check_availability()
try:
driver.find_element(*query('next_dates_btn')).click()
log_print('i', f"Getting more available dates...")
wait.until(element_to_be_clickable(query('available_days_container')))
except Exception as e:
break
if not found_available:
print()
wait_print(config["time_to_refresh"],
waiting_msg=f"No new date found or not a good option! Waiting {config['time_to_refresh']}s before refresh...",
end_msg="Refreshing the page!", log_type="w") # take a pause
driver.get(config['bupa_account_url'])
# driver.refresh() # Refresh the page and restart the loop which will look for the calendar
time.sleep(2) # take a pause
def book_appointment(driver, config, new_date):
try:
wait.until(element_to_be_clickable(query('time_radio'))) # query('time_radio') = (By.CSS_SELECTOR, '#ContentPlaceHolder1_SelectTime1_rblResults input')
time_results = driver.find_elements(*query('time_radio'))
except Exception as e:
log_print("e", "No times found for current selected date", end="")
return False
best_datetime = None
for e in time_results:
new_time = datetime.datetime.strptime(e.get_attribute('data-text'), "%I:%M %p")
new_date = new_date.replace(hour=new_time.hour, minute=new_time.minute)
log_print("w", f"Checking date-time: {new_date}")
if new_date < config['app_date']:
best_datetime = new_date
break
log_print("w", f"New date-time {new_date} is AFTER current appointment date: {config['app_date']}")
if best_datetime is None:
log_print("w", f"No suitable date-time found")
return False
log_print("w", f"Found a new suitable date-time! BOOKING {best_datetime}")
notify_telegram(f"Found a new suitable date-time: {best_datetime}")
if not config['dry_run']:
# And because I'm lazy, I'll always click the last radio button in the list (so the latest time of the day available)
# After clicking we need to wait to be able to click next
next_btn_edit = wait.until(element_to_be_clickable(query('next_btn'))) # query('next_btn') = (By.ID, 'ContentPlaceHolder1_btnCont')
next_btn_edit.click()
# After clicking next we need to wait until the confirmation page shows up
# Because the Save Changes button has no class or id I used XPATH to get the button with that specific text in it
save_btn = wait.until(element_to_be_clickable(query('save_btn'))) # (By.XPATH, '//button[text()="Save changes"]')
log_print("i", f"Found Save button! {save_btn.get_attribute('onclick')}")
save_btn.click() # Click Save!
# Update the date in the yaml file so at the next loop it won't keep booking this same one and get stuck in a loop
config['app_date'] = new_date
yaml.dump(config, open("config.yml", "w"))
notify_telegram(f"New appointment booked!: {new_date}")
return True
def check_date(date_to_check, curr_appt_date):
log_print("i", f"Checking new potential date: {date_to_check}", end="... ")
if date_to_check > curr_appt_date:
log_print("w", f"Date is AFTER the current booked date ({config['app_date']})", end="", start="", with_time=False)
return False
dates_range=config.get("dates_range")
from_date = None
to_date = None
if dates_range is not None:
from_date= dates_range.get("from")
to_date= dates_range.get("to")
if from_date is not None and date_to_check < datetime.datetime.strptime(from_date, '%d/%m/%Y'):
log_print("w", f"New date {date_to_check} is OUTSIDE the RANGE (BEFORE the FROM date {from_date})", end="")
return False
if to_date is not None and date_to_check > datetime.datetime.strptime(to_date, '%d/%m/%Y'):
log_print("w", f"New date {date_to_check} is OUTSIDE the RANGE (AFTER the TO date {to_date})", end="")
return False
weekdays_excluded = config.get("dates_weekdays_excluded")
if weekdays_excluded is not None:
for weekday_str in weekdays_excluded:
new_weekday = calendar.day_name[date_to_check.weekday()].lower()
weekday_ex = weekday_str.lower()
# log_print(f"{new_weekday} - {weekday_str}", end="\t")
if new_weekday == weekday_ex:
log_print("w", f"New date {date_to_check} is a {weekday_str.upper()}, which is EXCLUDED", end="")
return False
dates_excluded = config.get("dates_excluded")
if dates_excluded is not None:
for date_ex_str in dates_excluded:
date_ex = datetime.datetime.strptime(date_ex_str, '%d/%m/%Y')
if date_ex == date_to_check:
log_print("w", f"New date {date_to_check} is in the EXCLUDED list", end="")
return False
log_print("ok", f"New date {date_to_check} WORKS! Checking times...!", end="")
return True
def test_dates(dates_to_check):
for date in dates_to_check:
check_date(datetime.datetime.strptime(date, '%d/%m/%Y'))
"""
Function to login by inputting the data and moving to the edit booking page
"""
def login():
driver.get(config["bupa_base_url"]) # Navigate to the BUPA modify booking page
# Get all the field and send the input for each one
# driver.maximize_window()
time.sleep(2)
search_btn = wait.until(element_to_be_clickable(query('search_btn'))) # (By.ID, "ContentPlaceHolder1_btnSearch")
hapid = driver.find_element(*query('hapid_field')) # By.ID, 'ContentPlaceHolder1_txtHAPID'
hapid.send_keys(info['HAPID'])
firstname = driver.find_element(*query('first_name')) # By.ID, 'ContentPlaceHolder1_txtFirstName'
firstname.send_keys(info["first_name"])
surname = driver.find_element(*query('surname')) # By.ID, 'ContentPlaceHolder1_txtSurname'
surname.send_keys(info["surname"])
dob = driver.find_element(*query('dob')) # By.ID, 'ContentPlaceHolder1_txtDOB'
dob.send_keys(info["dob"])
# Finally click search to get to our booking
search_btn.click()
if __name__ == "__main__":
if not os.path.exists(logs_dir):
os.makedirs(logs_dir)
log_file = open(f"{logs_dir}/log_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}.log", "w")
read_config()
# test_dates(["01/01/2021", "29/01/2022", "30/01/2022", "31/12/2021", "01/01/2022", "01/03/2022", "10/02/2022", "01/02/2022", "15/01/2022", "12/12/2022", "01/01/2023"])
init_chrome_driver()
try:
loop_refresh()
except Exception as e:
log_print("e", f"Critical ERROR!! {e}", end="")
print()
print(traceback.print_exception(e))
log_file.close()
bupa_base_url: https://bmvs.onlineappointmentscheduling.net.au/oasis/Search.aspx
bupa_account_url: https://bmvs.onlineappointmentscheduling.net.au/oasis/SearchResults.aspx
time_to_refresh: 15
wait_element_time: 10
chrome_driver_path: C:\chromedriver\chromedriver.exe
dry_run: false
date_format: '%d/%m/%Y'
dates_range: null
dates_weekdays_excluded: null
dates_excluded: null
selectors:
search_btn:
by: id
query: ContentPlaceHolder1_btnSearch
hapid_field:
by: id
query: txtHAPID
first_name:
by: id
query: txtFirstName
dob:
by: id
query: txtDOB
surname:
by: id
query: txtSurname
time_radio:
by: css selector
query: '#ContentPlaceHolder1_SelectTime1_divSearchResults input'
current_appointment:
by: css selector
query: .appointments-row .fLeft
calendar_date:
by: id
query: ContentPlaceHolder1_SelectTime1_txtAppDate
available_days_container:
by: id
query: divPaginationNavigation
available_days:
by: css selector
query: '#divPaginationNavigation button'
selected_day:
by: css selector
query: .Highlighted:not(.ui-datepicker-current-day)
edit_btn:
by: id
query: ContentPlaceHolder1_repAppointments_lnkChangeAppointment_0
next_dates_btn:
by: id
query: btnNextPagination
next_btn:
by: id
query: ContentPlaceHolder1_btnCont
save_btn:
by: xpath
query: //button[text()="Save changes"]
app_date: 2024-06-04 10:00:00
name: web
channels:
- conda-forge
- defaults
dependencies:
- ca-certificates=2021.10.8=h5b45459_0
- openssl=3.0.0=h8ffe710_2
- oyaml=1.0=pyhd8ed1ab_0
- pip=21.2.4=pyhd8ed1ab_0
- python=3.8.12=h900ac77_1_cpython
- python-chromedriver-binary=98.0.4758.48.0=py38haa244fe_0
- python_abi=3.8=2_cp38
- pyyaml=6.0=py38h294d835_3
- setuptools=58.2.0=py38haa244fe_0
- sqlite=3.36.0=h8ffe710_2
- ucrt=10.0.20348.0=h57928b3_0
- vc=14.2=hb210afc_5
- vs2015_runtime=14.29.30037=h902a5da_5
- wheel=0.37.0=pyhd8ed1ab_1
- yaml=0.2.5=h8ffe710_2
- pip:
- amazoncaptcha==0.5.2
- certifi==2021.5.30
- chardet==4.0.0
- colorama==0.4.4
- configparser==5.0.2
- crayons==0.4.0
- distlib==0.3.4
- filelock==3.4.2
- idna==2.10
- logger==1.4
- pillow==8.3.2
- platformdirs==2.4.1
- python-dotenv==0.15.0
- requests==2.25.1
- selenium==3.141.0
- six==1.16.0
- urllib3==1.26.7
- virtualenv==20.13.0
- webdriver-manager==3.4.2
prefix: D:\Programs\Anaconda3\envs\web
# Booking details
HAPID: '12345678'
first_name: Jane
surname: Doe
dob: 01/01/1970
telegram_token: "12345678:xxxxxxxx"
chat_id: "12345678"
alabaster @ file:///home/ktietz/src/ci/alabaster_1611921544520/work
anaconda-client==1.7.2
anaconda-navigator==2.0.3
anaconda-project @ file:///tmp/build/80754af9/anaconda-project_1610472525955/work
anyio @ file:///C:/ci/anyio_1620153418380/work/dist
appdirs==1.4.4
argh==0.26.2
argon2-cffi @ file:///C:/ci/argon2-cffi_1613037959010/work
asn1crypto @ file:///tmp/build/80754af9/asn1crypto_1596577642040/work
astroid @ file:///C:/ci/astroid_1613501047216/work
astropy @ file:///C:/ci/astropy_1617745647203/work
async-generator @ file:///home/ktietz/src/ci/async_generator_1611927993394/work
atomicwrites==1.4.0
attrs @ file:///tmp/build/80754af9/attrs_1604765588209/work
autopep8 @ file:///tmp/build/80754af9/autopep8_1615918855173/work
Babel @ file:///tmp/build/80754af9/babel_1607110387436/work
backcall @ file:///home/ktietz/src/ci/backcall_1611930011877/work
backports.functools-lru-cache @ file:///tmp/build/80754af9/backports.functools_lru_cache_1618170165463/work
backports.shutil-get-terminal-size @ file:///tmp/build/80754af9/backports.shutil_get_terminal_size_1608222128777/work
backports.tempfile @ file:///home/linux1/recipes/ci/backports.tempfile_1610991236607/work
backports.weakref==1.0.post1
bcrypt @ file:///C:/ci/bcrypt_1597936263757/work
beautifulsoup4 @ file:///home/linux1/recipes/ci/beautifulsoup4_1610988766420/work
bitarray @ file:///C:/ci/bitarray_1618435038389/work
bkcharts==0.2
black==19.10b0
bleach @ file:///tmp/build/80754af9/bleach_1612211392645/work
bokeh @ file:///C:/ci/bokeh_1620784067744/work
boto==2.49.0
Bottleneck==1.3.2
brotlipy==0.7.0
certifi==2020.12.5
cffi @ file:///C:/ci/cffi_1613247279197/work
chardet @ file:///C:/ci/chardet_1607690654534/work
chromedriver-binary==97.0.4692.36.0
click @ file:///home/linux1/recipes/ci/click_1610990599742/work
cloudpickle @ file:///tmp/build/80754af9/cloudpickle_1598884132938/work
clyent==1.2.2
colorama @ file:///tmp/build/80754af9/colorama_1607707115595/work
comtypes==1.1.9
conda==4.11.0
conda-build==3.21.4
conda-content-trust @ file:///tmp/build/80754af9/conda-content-trust_1617045594566/work
conda-package-handling @ file:///C:/ci/conda-package-handling_1618262320430/work
conda-repo-cli @ file:///tmp/build/80754af9/conda-repo-cli_1620168426516/work
conda-token @ file:///tmp/build/80754af9/conda-token_1620076980546/work
conda-verify==3.4.2
contextlib2==0.6.0.post1
cryptography @ file:///C:/ci/cryptography_1616769344312/work
cycler==0.10.0
Cython @ file:///C:/ci/cython_1618435363327/work
cytoolz==0.11.0
dask @ file:///tmp/build/80754af9/dask-core_1617390489108/work
decorator @ file:///tmp/build/80754af9/decorator_1617916966915/work
defusedxml @ file:///tmp/build/80754af9/defusedxml_1615228127516/work
diff-match-patch @ file:///tmp/build/80754af9/diff-match-patch_1594828741838/work
distributed @ file:///C:/ci/distributed_1617384289923/work
docutils @ file:///C:/ci/docutils_1617481617511/work
entrypoints==0.3
et-xmlfile==1.0.1
fastcache==1.1.0
filelock @ file:///home/linux1/recipes/ci/filelock_1610993975404/work
flake8 @ file:///tmp/build/80754af9/flake8_1615834841867/work
Flask @ file:///home/ktietz/src/ci/flask_1611932660458/work
fsspec @ file:///tmp/build/80754af9/fsspec_1617959894824/work
future==0.18.2
gevent @ file:///C:/ci/gevent_1616773090559/work
glob2 @ file:///home/linux1/recipes/ci/glob2_1610991677669/work
greenlet @ file:///C:/ci/greenlet_1611958565931/work
h11==0.12.0
h5py==2.10.0
HeapDict==1.0.1
html5lib @ file:///tmp/build/80754af9/html5lib_1593446221756/work
idna @ file:///home/linux1/recipes/ci/idna_1610986105248/work
imagecodecs @ file:///C:/ci/imagecodecs_1617996768495/work
imageio @ file:///tmp/build/80754af9/imageio_1617700267927/work
imagesize @ file:///home/ktietz/src/ci/imagesize_1611921604382/work
importlib-metadata @ file:///C:/ci/importlib-metadata_1617877484576/work
iniconfig @ file:///home/linux1/recipes/ci/iniconfig_1610983019677/work
intervaltree @ file:///tmp/build/80754af9/intervaltree_1598376443606/work
ipykernel @ file:///C:/ci/ipykernel_1596190155316/work/dist/ipykernel-5.3.4-py3-none-any.whl
ipython @ file:///C:/ci/ipython_1617121002983/work
ipython-genutils @ file:///tmp/build/80754af9/ipython_genutils_1606773439826/work
ipywidgets @ file:///tmp/build/80754af9/ipywidgets_1610481889018/work
isort @ file:///tmp/build/80754af9/isort_1616355431277/work
itsdangerous @ file:///home/ktietz/src/ci/itsdangerous_1611932585308/work
jdcal==1.4.1
jedi @ file:///C:/ci/jedi_1606914528444/work
Jinja2 @ file:///tmp/build/80754af9/jinja2_1612213139570/work
joblib @ file:///tmp/build/80754af9/joblib_1613502643832/work
json5==0.9.5
jsonschema @ file:///tmp/build/80754af9/jsonschema_1602607155483/work
jupyter==1.0.0
jupyter-client @ file:///tmp/build/80754af9/jupyter_client_1616770841739/work
jupyter-console @ file:///tmp/build/80754af9/jupyter_console_1616615302928/work
jupyter-core @ file:///C:/ci/jupyter_core_1612213356021/work
jupyter-packaging @ file:///tmp/build/80754af9/jupyter-packaging_1613502826984/work
jupyter-server @ file:///C:/ci/jupyter_server_1616084298403/work
jupyterlab @ file:///tmp/build/80754af9/jupyterlab_1619133235951/work
jupyterlab-pygments @ file:///tmp/build/80754af9/jupyterlab_pygments_1601490720602/work
jupyterlab-server @ file:///tmp/build/80754af9/jupyterlab_server_1617134334258/work
jupyterlab-widgets @ file:///tmp/build/80754af9/jupyterlab_widgets_1609884341231/work
keyring @ file:///C:/ci/keyring_1614616910860/work
kiwisolver @ file:///C:/ci/kiwisolver_1612282606037/work
lazy-object-proxy @ file:///C:/ci/lazy-object-proxy_1616529307648/work
libarchive-c @ file:///tmp/build/80754af9/python-libarchive-c_1617780486945/work
llvmlite==0.36.0
locket==0.2.1
lxml @ file:///C:/ci/lxml_1616443455957/work
MarkupSafe==1.1.1
matplotlib @ file:///C:/ci/matplotlib-suite_1613408055530/work
mccabe==0.6.1
menuinst==1.4.16
mistune==0.8.4
mkl-fft==1.3.0
mkl-random @ file:///C:/ci/mkl_random_1618854156666/work
mkl-service==2.3.0
mock @ file:///tmp/build/80754af9/mock_1607622725907/work
more-itertools @ file:///tmp/build/80754af9/more-itertools_1613676688952/work
mpmath==1.2.1
msgpack @ file:///C:/ci/msgpack-python_1612287368835/work
multipledispatch==0.6.0
mypy-extensions==0.4.3
navigator-updater==0.2.1
nbclassic @ file:///tmp/build/80754af9/nbclassic_1616085367084/work
nbclient @ file:///tmp/build/80754af9/nbclient_1614364831625/work
nbconvert @ file:///C:/ci/nbconvert_1601914925608/work
nbformat @ file:///tmp/build/80754af9/nbformat_1617383369282/work
nest-asyncio @ file:///tmp/build/80754af9/nest-asyncio_1613680548246/work
networkx @ file:///tmp/build/80754af9/networkx_1598376031484/work
nltk @ file:///tmp/build/80754af9/nltk_1618327084230/work
nose @ file:///tmp/build/80754af9/nose_1606773131901/work
notebook @ file:///C:/ci/notebook_1616443715883/work
numba @ file:///C:/ci/numba_1616774458845/work
numexpr @ file:///C:/ci/numexpr_1618856738664/work
numpy @ file:///C:/ci/numpy_and_numpy_base_1618497418457/work
numpydoc @ file:///tmp/build/80754af9/numpydoc_1605117425582/work
olefile==0.46
openpyxl @ file:///tmp/build/80754af9/openpyxl_1615411699337/work
outcome==1.1.0
oyaml==1.0
packaging @ file:///tmp/build/80754af9/packaging_1611952188834/work
pandas @ file:///C:/ci/pandas_1618365634936/work
pandocfilters @ file:///C:/ci/pandocfilters_1605102497129/work
paramiko @ file:///tmp/build/80754af9/paramiko_1598886428689/work
parso==0.7.0
partd @ file:///tmp/build/80754af9/partd_1618000087440/work
path @ file:///C:/ci/path_1614022440181/work
pathlib2 @ file:///C:/ci/pathlib2_1607025069150/work
pathspec==0.7.0
patsy==0.5.1
pep8==1.7.1
pexpect @ file:///tmp/build/80754af9/pexpect_1605563209008/work
pickleshare @ file:///tmp/build/80754af9/pickleshare_1606932040724/work
Pillow @ file:///C:/ci/pillow_1617386341487/work
pkginfo==1.7.0
pluggy @ file:///C:/ci/pluggy_1615976358795/work
ply==3.11
prometheus-client @ file:///tmp/build/80754af9/prometheus_client_1618088486455/work
prompt-toolkit @ file:///tmp/build/80754af9/prompt-toolkit_1616415428029/work
psutil @ file:///C:/ci/psutil_1612298324802/work
ptyprocess @ file:///tmp/build/80754af9/ptyprocess_1609355006118/work/dist/ptyprocess-0.7.0-py2.py3-none-any.whl
py @ file:///tmp/build/80754af9/py_1607971587848/work
pycodestyle @ file:///home/ktietz/src/ci_mi/pycodestyle_1612807597675/work
pycosat==0.6.3
pycparser @ file:///tmp/build/80754af9/pycparser_1594388511720/work
pycurl==7.43.0.6
pydocstyle @ file:///tmp/build/80754af9/pydocstyle_1616182067796/work
pyerfa @ file:///C:/ci/pyerfa_1619391071834/work
pyflakes @ file:///home/ktietz/src/ci_ipy2/pyflakes_1612551159640/work
Pygments @ file:///tmp/build/80754af9/pygments_1615143339740/work
pylint @ file:///C:/ci/pylint_1617136058775/work
pyls-black @ file:///tmp/build/80754af9/pyls-black_1607553132291/work
pyls-spyder @ file:///tmp/build/80754af9/pyls-spyder_1613849700860/work
PyNaCl @ file:///C:/ci/pynacl_1595000047588/work
pyodbc===4.0.0-unsupported
pyOpenSSL @ file:///tmp/build/80754af9/pyopenssl_1608057966937/work
pyparsing @ file:///home/linux1/recipes/ci/pyparsing_1610983426697/work
pyreadline==2.1
pyrsistent @ file:///C:/ci/pyrsistent_1600141795814/work
PySocks @ file:///C:/ci/pysocks_1605287845585/work
pytest==6.2.3
python-dateutil @ file:///home/ktietz/src/ci/python-dateutil_1611928101742/work
python-jsonrpc-server @ file:///tmp/build/80754af9/python-jsonrpc-server_1600278539111/work
python-language-server @ file:///tmp/build/80754af9/python-language-server_1607972495879/work
pytz @ file:///tmp/build/80754af9/pytz_1612215392582/work
PyWavelets @ file:///C:/ci/pywavelets_1601658407916/work
pywin32==227
pywin32-ctypes==0.2.0
pywinpty==0.5.7
PyYAML==5.4.1
pyzmq==20.0.0
QDarkStyle==2.8.1
QtAwesome @ file:///tmp/build/80754af9/qtawesome_1615991616277/work
qtconsole @ file:///tmp/build/80754af9/qtconsole_1616775094278/work
QtPy==1.9.0
regex @ file:///C:/ci/regex_1617569893741/work
requests @ file:///tmp/build/80754af9/requests_1608241421344/work
rope @ file:///tmp/build/80754af9/rope_1602264064449/work
Rtree @ file:///C:/ci/rtree_1618421009405/work
ruamel-yaml-conda @ file:///C:/ci/ruamel_yaml_1616016967756/work
scikit-image==0.18.1
scikit-learn @ file:///C:/ci/scikit-learn_1614446896245/work
scipy @ file:///C:/ci/scipy_1618856128765/work
seaborn @ file:///tmp/build/80754af9/seaborn_1608578541026/work
selenium==4.1.0
Send2Trash @ file:///tmp/build/80754af9/send2trash_1607525499227/work
simplegeneric==0.8.1
singledispatch @ file:///tmp/build/80754af9/singledispatch_1614366001199/work
sip==4.19.13
six @ file:///C:/ci/six_1605187374963/work
sniffio @ file:///C:/ci/sniffio_1614030707456/work
snowballstemmer @ file:///tmp/build/80754af9/snowballstemmer_1611258885636/work
sortedcollections @ file:///tmp/build/80754af9/sortedcollections_1611172717284/work
sortedcontainers @ file:///tmp/build/80754af9/sortedcontainers_1606865132123/work
soupsieve @ file:///tmp/build/80754af9/soupsieve_1616183228191/work
Sphinx @ file:///tmp/build/80754af9/sphinx_1620777493457/work
sphinxcontrib-applehelp @ file:///home/ktietz/src/ci/sphinxcontrib-applehelp_1611920841464/work
sphinxcontrib-devhelp @ file:///home/ktietz/src/ci/sphinxcontrib-devhelp_1611920923094/work
sphinxcontrib-htmlhelp @ file:///home/ktietz/src/ci/sphinxcontrib-htmlhelp_1611920974801/work
sphinxcontrib-jsmath @ file:///home/ktietz/src/ci/sphinxcontrib-jsmath_1611920942228/work
sphinxcontrib-qthelp @ file:///home/ktietz/src/ci/sphinxcontrib-qthelp_1611921055322/work
sphinxcontrib-serializinghtml @ file:///home/ktietz/src/ci/sphinxcontrib-serializinghtml_1611920755253/work
sphinxcontrib-websupport @ file:///tmp/build/80754af9/sphinxcontrib-websupport_1597081412696/work
spyder @ file:///C:/ci/spyder_1616776239898/work
spyder-kernels @ file:///C:/ci/spyder-kernels_1614030842607/work
SQLAlchemy @ file:///C:/ci/sqlalchemy_1618090063261/work
statsmodels==0.12.2
sympy @ file:///C:/ci/sympy_1618255511605/work
tables==3.6.1
tblib @ file:///tmp/build/80754af9/tblib_1597928476713/work
terminado==0.9.4
testpath @ file:///home/ktietz/src/ci/testpath_1611930608132/work
textdistance @ file:///tmp/build/80754af9/textdistance_1612461398012/work
threadpoolctl @ file:///tmp/tmp9twdgx9k/threadpoolctl-2.1.0-py3-none-any.whl
three-merge @ file:///tmp/build/80754af9/three-merge_1607553261110/work
tifffile @ file:///tmp/build/80754af9/tifffile_1619636090847/work
toml @ file:///tmp/build/80754af9/toml_1616166611790/work
toolz @ file:///home/linux1/recipes/ci/toolz_1610987900194/work
tornado @ file:///C:/ci/tornado_1606942392901/work
tqdm @ file:///tmp/build/80754af9/tqdm_1615925068909/work
traitlets @ file:///home/ktietz/src/ci/traitlets_1611929699868/work
trio==0.19.0
trio-websocket==0.9.2
typed-ast @ file:///C:/ci/typed-ast_1610466535590/work
typing-extensions @ file:///home/ktietz/src/ci_mi/typing_extensions_1612808209620/work
ujson @ file:///C:/ci/ujson_1611241570789/work
unicodecsv==0.14.1
urllib3 @ file:///tmp/build/80754af9/urllib3_1615837158687/work
watchdog @ file:///C:/ci/watchdog_1612471251191/work
wcwidth @ file:///tmp/build/80754af9/wcwidth_1593447189090/work
webencodings==0.5.1
Werkzeug @ file:///home/ktietz/src/ci/werkzeug_1611932622770/work
widgetsnbextension==3.5.1
win-inet-pton @ file:///C:/ci/win_inet_pton_1605306167264/work
win-unicode-console==0.5
wincertstore==0.2
wrapt==1.12.1
wsproto==1.0.0
xlrd @ file:///tmp/build/80754af9/xlrd_1608072521494/work
XlsxWriter @ file:///tmp/build/80754af9/xlsxwriter_1617224712951/work
xlwings==0.23.0
xlwt==1.3.0
xmltodict @ file:///Users/ktietz/demo/mc3/conda-bld/xmltodict_1629301980723/work
yapf @ file:///tmp/build/80754af9/yapf_1615749224965/work
zict==2.0.0
zipp @ file:///tmp/build/80754af9/zipp_1615904174917/work
zope.event==4.5.0
zope.interface @ file:///C:/ci/zope.interface_1616357322857/work
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment