39 Commits
Author SHA1 Message Date
sugoidogo 61c0e9bd62 allow running bot under alternate user
Release / release (push) Successful in 20s
2026-05-27 03:47:14 -07:00
sugoidogo d278df09f7 don't fail for update check 2025-12-14 22:16:59 -08:00
sugoidogo 8e31da1636 automated release 2025-12-14 22:16:23 -08:00
sugoidogo e4280f7721 update release url 2025-12-14 22:13:23 -08:00
sugoidogo c2638a03ad always call login if refreshing tokens fails 2025-12-12 23:43:38 -08:00
sugoidogo a793c96532 always open browser if login is called and device_code is availible, and print url to output in case webbrowser.open fails 2025-12-12 23:43:03 -08:00
sugoidogo 1ce6dc2de1 use specific version of twitch.py 2025-12-12 23:37:33 -08:00
sugoidogo e93f0e6473 add aspect transits 2025-07-29 10:35:33 -07:00
sugoidogo 4fbdfef3ff fix maxdays comparison 2025-07-29 08:10:16 -07:00
sugoidogo 38e031a9ba prevent deleting cached results 2025-07-29 08:03:47 -07:00
sugoidogo 38b93884c2 restore missing variable 2025-07-29 07:58:17 -07:00
sugoidogo 862279c39c add default values to non-obs settings 2025-07-29 07:57:29 -07:00
sugoidogo e9fdb3a04d maximize cache usage 2025-07-29 07:57:01 -07:00
sugoidogo 363a985b16 cache all date parameter functions 2025-07-29 07:32:10 -07:00
sugoidogo 5529bf1402 add aspects command 2025-07-27 22:14:09 -07:00
sugoidogo 173fa33b8f add aspect functions and refactor functions for reusable code 2025-07-24 21:12:53 -07:00
sugoidogo bbe85db026 reduce startup delay by skipping pip install if pip folder exists 2025-07-24 09:49:55 -07:00
sugoidogo 3437bb50a6 update in seperate thread 2025-07-20 23:18:24 -07:00
sugoidogo 4cf720cc5d fix typo 2025-07-20 23:15:26 -07:00
sugoidogo 0e16e196b4 fix update function args 2025-07-20 23:14:19 -07:00
sugoidogo 70616babbd add update button to obs 2025-07-20 23:12:04 -07:00
sugoidogo 3cd0ab79a8 add command name to settings 2025-07-20 22:51:06 -07:00
sugoidogo 3182d66e63 fix early login request 2025-07-20 15:57:49 -07:00
sugoidogo e60ff257e6 gracefully fail refreshing tokens 2025-07-20 15:55:39 -07:00
sugoidogo ef93c95f9d refresh tokens and log token activity 2025-07-20 02:59:32 -07:00
sugoidogo fb72c54ab7 use universal strftime format 2025-07-19 01:57:37 -07:00
sugoidogo a3915595e3 switch to twitch.py and other improvements 2025-07-19 01:31:50 -07:00
sugoidogo 7cabd2e1b3 fix overridden event 2025-07-18 17:10:36 -07:00
sugoidogo 011deb41e0 fix dependency check 2025-07-18 01:39:05 -07:00
sugoidogo 0e67657698 better dependency check 2025-07-18 01:35:27 -07:00
sugoidogo bb05fc0b44 fix loading with no tokens 2025-07-18 01:19:02 -07:00
sugoidogo ee68221691 fix library usage for OBS cold start 2025-07-18 01:03:56 -07:00
sugoidogo fcc0d8649b add !transits command 2025-07-18 00:25:48 -07:00
sugoidogo 9695f0bc10 updated output format 2025-07-16 21:18:54 -07:00
sugoidogo 742ae622d2 install dependencies to astrolobot folder 2025-07-16 20:21:55 -07:00
sugoidogo 973ef9d338 more readable output format 2025-07-16 11:36:28 -07:00
sugoidogo f8861b130d add simple test without OBS 2025-07-16 11:36:09 -07:00
sugoidogo 967d5aa16a download swiss ephemeris files as needed 2025-07-16 11:35:34 -07:00
sugoidogo 08f2924c2a initial commit 2025-07-16 03:49:20 -07:00
3 changed files with 403 additions and 109 deletions
+23
View File
@@ -0,0 +1,23 @@
name: Release
on:
push:
tags:
- "*"
permissions:
contents: write
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
with:
fetch-depth: 0
- run: git log $(git describe --tags --abbrev=0 HEAD~1)..HEAD --oneline > changelog.txt
- uses: softprops/action-gh-release@v2
with:
make_latest: true
body_path: changelog.txt
files: astrolobot.py
+365 -94
View File
@@ -5,14 +5,16 @@ from urllib.parse import quote_plus
from urllib.request import urlretrieve, urlopen, Request, HTTPError
from threading import Thread
from os import makedirs,path
from functools import cache
import json,sys,webbrowser,importlib,tomllib
def update(*args):
def update():
print('checking for updates...')
try:
with open(script_path()+'astrolobot.py','r') as file:
current_script=file.read()
urlretrieve(
'https://github.com/sugoidogo/astrolobot/releases/latest/download/astrolobot.py',
'https://gitea.sugoidogo.com/SugoiDogo/astrolobot/releases/download/latest/astrolobot.py',
script_path()+'astrolobot.py'
)
with open(script_path()+'astrolobot.py','r') as file:
@@ -21,11 +23,15 @@ def update(*args):
print('up to date')
else:
print('update downloaded, restart script to apply')
except:
print('update check failed, ignoring')
def dependency_setup():
print('checking dependencies')
if not path.exists(script_path()+'pip'):
print('installing dependencies')
from pip._internal.cli.main import main as pip
pip(['install','-qq','pyswisseph','twitch.py','--target',script_path()+'pip'])
pip(['install','-qq','-r',script_path()+'/requirements.txt','--target',script_path()+'pip'])
sys.path.append(script_path()+'pip')
if not path.exists(script_path()+'ephe/seas_18.se1'):
@@ -33,8 +39,16 @@ def dependency_setup():
makedirs(script_path()+'ephe')
urlretrieve('https://github.com/aloistr/swisseph/raw/refs/heads/master/ephe/seas_18.se1',script_path()+'ephe/seas_18.se1')
import swisseph as swe
swe.set_ephe_path(script_path()+'ephe/')
swe.set_sid_mode(swe.SIDM_LAHIRI)
print('dependencies ready')
def today():
now=datetime.utcnow()
return datetime(now.year,now.month,now.day)
# Astrology functions
def load_settings():
@@ -45,20 +59,24 @@ def load_settings():
return {
'positions':'!positions',
'transits':'!transits',
'major_aspects':'!aspects major',
'minor_aspects':'!aspects minor',
'major_aspect_transits':'!aspects major transits',
'minor_aspect_transits':'!aspects minor transits',
}
zodiac_signs = ["Aries", "Taurus", "Gemini", "Cancer", "Leo", "Virgo",
def get_zodiac(angle):
zodiac = ["Aries", "Taurus", "Gemini", "Cancer", "Leo", "Virgo",
"Libra", "Scorpio", "Sagittarius", "Capricorn", "Aquarius", "Pisces"]
return zodiac[int(angle//30)]
def get_planet_info(date):
def is_retrograde(speed):
return speed<0
@cache
def get_positions_raw(date=today()):
import swisseph as swe
# make sure location is set in THIS thread
swe.set_ephe_path(script_path()+'ephe/')
swe.set_sid_mode(swe.SIDM_LAHIRI)
# Get current UTC time
julian_day = swe.julday(date.year, date.month, date.day)
# List of planets (Swiss Ephemeris IDs)
planets = {
"The Sun": swe.SUN,
"The Moon": swe.MOON,
@@ -73,93 +91,303 @@ def get_planet_info(date):
"Chiron": swe.CHIRON,
"North Node":swe.TRUE_NODE
}
positions={}
retrograde_info = []
positions_info = {}
for name, planet_id in planets.items():
# Get planet position (longitude) and speed
pos, flags = swe.calc_ut(julian_day, planet_id, swe.FLG_SPEED)
# Check if retrograde (speed is negative)
is_retrograde = pos[3] < 0
if is_retrograde:
if name=='North Node':
pass
else:
retrograde_info.append(name)
# Get zodiac sign (0-360° -> Aries to Pisces)
sign_num = int(pos[0] // 30) # 30° per sign
positions_info[name]=sign_num
if name=='North Node':
# rotate sign 180 dagrees
south_pos=pos[0]+180
if(south_pos>360):
south_pos-=360
# calculate South Node sign
sign_num = int(south_pos // 30)
positions_info['South Node']=sign_num
return {
'positions': positions_info,
'retrograde': retrograde_info,
for planet_name, planet_id in planets.items():
position, _ = swe.calc_ut(julian_day, planet_id, swe.FLG_SPEED)
positions[planet_name]={
'angle':position[0],
'speed':position[3],
}
if planet_name=='North Node':
positions['South Node']={
'angle':(position[0]+180)%360,
'speed':position[3],
}
def get_planet_info_formatted(date=datetime.utcnow()):
planets=get_planet_info(date)
return positions
match len(planets['retrograde']):
@cache
def get_positions(date=today()):
positions={}
for name, position_raw in get_positions_raw(date).items():
positions[name]={
'zodiac':get_zodiac(position_raw['angle']),
'retrograde':is_retrograde(position_raw['speed']),
}
return positions
def get_list_formatted(in_list,isare=False):
match len(in_list):
case 0:
info=''
output='Nothing'
if isare:
output+=' is '
case 1:
info=planets['retrograde'][0]+' is in Retrograde\n'
output=in_list[0]
if isare:
output+=' is '
case 2:
info=' and '.join(planets['retrograde'])+' are in Retrograde\n'
output=' and '.join(in_list)
if isare:
output+=' are '
case _:
info=', '.join(planets['retrograde'][0:-1])+', and '+planets['retrograde'][-1]+' are in Retrograde\n'
output=', '.join(in_list[0:-1])+', and '+in_list[-1]
if isare:
output+=' are '
return output
positions_info=[]
for planet,position in planets['positions'].items():
positions_info.append(planet+' is in '+zodiac_signs[position])
@cache
def get_positions_formatted(date=today()):
retrograde_planets=[]
position_strings=[]
info+=', '.join(positions_info[0:2])+'\n'
info+=', '.join(positions_info[2:5])+'\n'
info+=', '.join(positions_info[5:8])+'\n'
info+=', '.join(positions_info[8:11])+'\n'
info+=', '.join(positions_info[11:13])+'\n'
return info
for name, position in get_positions(date).items():
if(position['retrograde']):
retrograde_planets.append(name)
position_strings.append(name+' is in '+position['zodiac'])
def get_transits():
seconds_in_1_day=86400
now=datetime.utcnow()
positions_today=get_planet_info(now)
position_updates={}
positions_formatted=get_list_formatted(retrograde_planets,True)+'in Retrograde\n'
positions_formatted+=', '.join(position_strings[0:2])+'\n'
positions_formatted+=', '.join(position_strings[2:5])+'\n'
positions_formatted+=', '.join(position_strings[5:8])+'\n'
positions_formatted+=', '.join(position_strings[8:11])+'\n'
positions_formatted+=', '.join(position_strings[11:13])+'\n'
return positions_formatted
seconds_in_1_day=86400
@cache
def get_transits(date=today(), maxdays=28):
positions_now=get_positions(date)
transits={}
days=1
date_format='%b %d'
while len(position_updates)<12 and days<29:
future_timestamp=now.timestamp()+(days*seconds_in_1_day)
future_date=datetime.fromtimestamp(future_timestamp,timezone.utc)
positions_future=get_planet_info(future_date)
for planet in positions_future['retrograde']:
if planet not in positions_today['retrograde'] and planet not in position_updates.keys():
position_updates[planet]='entering Retrograde on '+future_date.strftime(date_format)
for planet in positions_today['retrograde']:
if planet not in positions_future['retrograde'] and planet not in position_updates.keys():
position_updates[planet]='exiting Retrograde on '+future_date.strftime(date_format)
for planet in positions_future['positions']:
if positions_today['positions'][planet]!=positions_future['positions'][planet] and planet not in position_updates.keys():
position_updates[planet]='entering '+zodiac_signs[positions_future['positions'][planet]]+' on '+future_date.strftime(date_format)
days+=1
return position_updates
def get_transits_formatted():
transits=get_transits()
info=''
for planet,transit in transits.items():
info+=planet+' is '+transit+'\n'
return info
while len(transits)<12 and days<=maxdays:
future_timestamp=date.timestamp()+(days*seconds_in_1_day)
future_date=datetime.fromtimestamp(future_timestamp,timezone.utc)
positions_future=get_positions(future_date)
for name, position_future in positions_future.items():
position_now=positions_now[name]
if name not in transits and position_now!=position_future:
position_future['date']=future_date
if position_now['zodiac']==position_future['zodiac']:
position_future['zodiac']=None
if position_now['retrograde']==position_future['retrograde']:
position_future['retrograde']=None
transits[name]=position_future
days+=1
return transits
date_format='%b %d'
@cache
def get_transits_formatted(date=today(), maxdays=28):
transits_formatted=''
for name,transit in get_transits(date, maxdays).items():
if transit['zodiac']!=None:
transits_formatted+=name+' is entering '+transit['zodiac']+' on '+transit['date'].strftime(date_format)+'\n'
if transit['retrograde']==True:
transits_formatted+=name+' is entering Retrograde on '+transit['date'].strftime(date_format)+'\n'
if transit['retrograde']==False:
transits_formatted+=name+' is exiting Retrograde on '+transit['date'].strftime(date_format)+'\n'
return transits_formatted
@cache
def get_aspects(date=today(), minor=False):
positions=get_positions_raw(date).copy()
major_aspects={
'conjuction':{'angle':0,'orb':10.0},
'oposition':{'angle':180,'orb':10.0},
'trine':{'angle':120,'orb':10.0},
'square':{'angle':90,'orb':10.0},
'sextile':{'angle':60,'orb':5.0},
}
minor_aspects={
'semi-sextile':{'angle':30,'orb':1.5},
'inconjunct':{'angle':150,'orb':3},
'semi-square':{'angle':45,'orb':3},
'trioctile':{'angle':135,'orb':3},
'quintile':{'angle':72,'orb':1},
'biquintile':{'angle':144,'orb':1},
}
aspects={}
if minor:
selected_aspects=minor_aspects
else:
selected_aspects=major_aspects
for aname, aposition in positions.copy().items():
del positions[aname]
if aname.endswith('Node'):
continue
planet_aspects={}
for bname, bposition in positions.items():
angle_diff=abs(aposition['angle']-bposition['angle'])
for aspect_name,aspect in selected_aspects.items():
aspect_max=(aspect['angle']+aspect['orb'])%360
aspect_min=(aspect['angle']-aspect['orb'])%360
if aspect_min<angle_diff<aspect_max:
try:
planet_aspects[aspect_name].append(bname)
except KeyError:
planet_aspects[aspect_name]=[bname]
if len(planet_aspects)!=0:
aspects[aname]=planet_aspects
return aspects
@cache
def get_aspects_formatted(date=today(),minor=False):
aspects_formatted=''
for planet,aspects in get_aspects(date,minor).items():
aspects_formatted+=planet+' is in '
aspects_list=[]
for aspect_name,aspect_planets in aspects.items():
aspects_list.append(aspect_name+' with '+get_list_formatted(aspect_planets))
aspects_formatted+=get_list_formatted(aspects_list)+'\n'
return aspects_formatted
@cache
def get_aspect_transits(date=today(),minor=False,maxdays=28):
date_now=today()
timestamp_now=date_now.timestamp()
aspects_now=get_aspects(date_now,minor)
pre_transits={}
days=0
def aspects_contains(aspects,planet_a,aspect=None,planet_b=None):
if planet_a not in aspects:
return False
elif aspect and aspect not in aspects[planet_a]:
return False
elif aspect and planet_b and planet_b not in aspects[planet_a][aspect]:
return False
return True
def ensure_pre_path(planet_a,aspect=None):
if planet_a not in pre_transits:
pre_transits[planet_a]={}
if aspect and aspect not in pre_transits[planet_a]:
pre_transits[planet_a][aspect]={}
def pre_planet_b(planet_a,aspect,planet_b,date_future,direction):
ensure_pre_path(planet_a,aspect)
pre_transits[planet_a][aspect][planet_b]={
'date':date_future,
'direction':direction
}
def pre_aspect(planet_a,aspect,planets,date_future,direction):
ensure_pre_path(planet_a)
pre_transits[planet_a][aspect]={}
for planet_b in planets:
pre_planet_b(planet_a,aspect,planet_b,date_future,direction)
def pre_planet_a(planet_a,aspects,date_future,direction):
pre_transits[planet_a]={}
for aspect,planets in aspects.items():
pre_aspect(planet_a,aspect,planets,date_future,direction)
while days<maxdays:
days+=1
timestamp_future=timestamp_now+(seconds_in_1_day*days)
date_future=datetime.fromtimestamp(timestamp_future,timezone.utc)
aspects_future=get_aspects(date_future)
for planet_a,aspects in aspects_now.items():
try:
pre_transits[planet_a]
except KeyError:
if not aspects_contains(aspects_future,planet_a):
pre_planet_a(planet_a,aspects,date_future,'exiting')
continue
for aspect, planets in aspects.items():
try:
pre_transits[planet_a][aspect]
except KeyError:
if not aspects_contains(aspects_future,planet_a,aspect):
pre_aspect(planet_a,aspect,planets,date_future,'exiting')
continue
for planet_b in planets:
try:
pre_transits[planet_a][aspect][planet_b]
except KeyError:
if not aspects_contains(aspects_future,planet_a,aspect,planet_b):
pre_planet_b(planet_a,aspect,planet_b,date_future,'exiting')
days=0
while days<maxdays:
days+=1
timestamp_future=timestamp_now+(seconds_in_1_day*days)
date_future=datetime.fromtimestamp(timestamp_future,timezone.utc)
aspects_future=get_aspects(date_future)
for planet_a,aspects in aspects_future.items():
try:
pre_transits[planet_a]
except KeyError:
if not aspects_contains(aspects_now,planet_a):
pre_planet_a(planet_a,aspects,date_future,'entering')
continue
for aspect, planets in aspects.items():
try:
pre_transits[planet_a][aspect]
except KeyError:
if not aspects_contains(aspects_now,planet_a,aspect):
pre_aspect(planet_a,aspect,planets,date_future,'entering')
continue
for planet_b in planets:
try:
pre_transits[planet_a][aspect][planet_b]
except KeyError:
if not aspects_contains(aspects_now,planet_a,aspect,planet_b):
pre_planet_b(planet_a,aspect,planet_b,date_future,'entering')
post_transits={}
for planet_a,aspects in pre_transits.items():
post_transits[planet_a]={}
for aspect,planets in aspects.items():
for planet_b,transit in planets.items():
direction=transit['direction']
date=transit['date']
if direction not in post_transits[planet_a]:
post_transits[planet_a][direction]={}
if date not in post_transits[planet_a][direction]:
post_transits[planet_a][direction][date]={}
if aspect not in post_transits[planet_a][direction][date]:
post_transits[planet_a][direction][date][aspect]=[]
post_transits[planet_a][direction][date][aspect].append(planet_b)
return post_transits
@cache
def get_aspect_transits_formatted(date=today(),minor=False,maxdays=28):
aspect_transits=get_aspect_transits(date,minor,maxdays)
aspect_transits_formatted=''
for planet_a,directions in aspect_transits.items():
aspect_transits_formatted+=planet_a+' is '
direction_list=[]
for direction,dates in directions.items():
date_list=[]
for date,aspects in dates.items():
aspect_list=[]
for aspect,planets in aspects.items():
aspect_list.append(aspect+' with '+get_list_formatted(planets))
date_list.append(get_list_formatted(aspect_list)+' on '+date.strftime(date_format))
direction_list.append(direction+' '+get_list_formatted(date_list))
aspect_transits_formatted+=get_list_formatted(direction_list)+'\n'
return aspect_transits_formatted
# Twitch functions
@@ -169,7 +397,11 @@ def login(*args):
global device_code
if device_code==None:
print('astrolobot is still starting, please wait',sys.stderr)
webbrowser.open('https://www.twitch.tv/activate?public=true&device-code='+device_code)
else:
login_url='https://www.twitch.tv/activate?public=true&device-code='+device_code
print("if your browser doesn't automatically open, go to the following url:")
print(login_url)
webbrowser.open(login_url)
def save_tokens(access_token,refresh_token):
with open(script_path()+'tokens.json','w') as file:
@@ -238,7 +470,7 @@ def main():
async def on_code(code: str):
global device_code, obs_settings
device_code=code
if obs_settings == None:
if len(tokens)==0:
login()
@client.event
@@ -247,6 +479,12 @@ def main():
@client.event
async def on_ready() -> None:
global user
user=client.user
config=load_settings()
if('channel' in config):
user=await client.get_user(config['channel'])
await client.add_custom_event('on_chat_message',user,on_chat_message)
print('astrolobot ready')
while True:
tokens=client.http.get_token(client.user.id)
@@ -254,16 +492,33 @@ def main():
tokens=refresh_tokens(tokens['refresh_token'])
await client.authorize(tokens['access_token'],tokens['refresh_token'])
@client.event
async def on_chat_message(data: eventsub.chat.MessageEvent):
global user
config=load_settings()
if data['message']['text']==config['positions']:
for line in get_planet_info_formatted().splitlines(False):
await client.channel.chat.send_message(line,data['message_id'])
for line in get_positions_formatted().splitlines(False):
await user.channel.chat.send_message(line,data['message_id'])
return
if data['message']['text']==config['transits']:
for line in get_transits_formatted().splitlines(False):
await client.channel.chat.send_message(line,data['message_id'])
await user.channel.chat.send_message(line,data['message_id'])
return
if data['message']['text']==config['major_aspects']:
for line in get_aspects_formatted().splitlines(False):
await user.channel.chat.send_message(line,data['message_id'])
return
if data['message']['text']==config['minor_aspects']:
for line in get_aspects_formatted(minor=True).splitlines(False):
await user.channel.chat.send_message(line,data['message_id'])
return
if data['message']['text']==config['major_aspect_transits']:
for line in get_aspect_transits_formatted().splitlines(False):
await user.channel.chat.send_message(line,data['message_id'])
return
if data['message']['text']==config['minor_aspect_transits']:
for line in get_aspect_transits_formatted(minor=True).splitlines(False):
await user.channel.chat.send_message(line,data['message_id'])
return
client.run(*tokens.values())
@@ -275,6 +530,9 @@ def script_main():
dependency_setup()
main()
def obs_update(*args):
Thread(target=update,daemon=True).start()
def obs_load_settings():
import obspython
global obs_settings
@@ -309,10 +567,15 @@ def script_properties():
import obspython
properties=obspython.obs_properties_create()
obspython.obs_properties_add_button(properties,'login','Login to Twitch',login)
obspython.obs_properties_add_button(properties,'update','Check for Updates',update)
obspython.obs_properties_add_button(properties,'update','Check for Updates',obs_update)
obspython.obs_properties_add_text(properties,'channel','channel name',obspython.OBS_TEXT_DEFAULT)
obspython.obs_properties_add_text(properties,'commands','commands',obspython.OBS_TEXT_INFO)
obspython.obs_properties_add_text(properties,'positions','positions',obspython.OBS_TEXT_DEFAULT)
obspython.obs_properties_add_text(properties,'transits','transits',obspython.OBS_TEXT_DEFAULT)
obspython.obs_properties_add_text(properties,'major_aspects','major aspects',obspython.OBS_TEXT_DEFAULT)
obspython.obs_properties_add_text(properties,'minor_aspects','minor aspects',obspython.OBS_TEXT_DEFAULT)
obspython.obs_properties_add_text(properties,'major_aspect_transits','major aspect transits',obspython.OBS_TEXT_DEFAULT)
obspython.obs_properties_add_text(properties,'minor_aspect_transits','minor aspect transits',obspython.OBS_TEXT_DEFAULT)
return properties
def script_defaults(settings):
@@ -320,6 +583,10 @@ def script_defaults(settings):
obspython.obs_data_set_default_string(settings,'commands','you can change the commands below, they save automatically')
obspython.obs_data_set_default_string(settings,'positions','!positions')
obspython.obs_data_set_default_string(settings,'transits','!transits')
obspython.obs_data_set_default_string(settings,'major_aspects','!aspects major')
obspython.obs_data_set_default_string(settings,'minor_aspects','!aspects minor')
obspython.obs_data_set_default_string(settings,'major_aspect_transits','!aspects major transits')
obspython.obs_data_set_default_string(settings,'minor_aspect_transits','!aspects minor transits')
def script_load(settings):
global obs_settings, save_tokens, load_tokens, load_settings
@@ -350,6 +617,10 @@ if __name__ == '__main__':
def script_path():
return path.dirname(__file__)+'/'
dependency_setup()
print(get_planet_info_formatted())
print(get_positions_formatted())
print(get_transits_formatted())
print(get_aspects_formatted())
print(get_aspects_formatted(minor=True))
print(get_aspect_transits_formatted())
print(get_aspect_transits_formatted(minor=True))
main()
+1 -1
View File
@@ -1,2 +1,2 @@
pyswisseph
twitch.py
twitch.py == 3.*