Generated API objects

This commit is contained in:
Sumner Evans
2019-06-21 23:09:38 -06:00
parent 950a1ae948
commit 2036830ed3
6 changed files with 496 additions and 169 deletions

View File

@@ -7,7 +7,7 @@ represents those API objects in Python.
import re
from collections import defaultdict
from typing import cast, Dict, DefaultDict, Set, Match, Tuple, List, Union
from typing import cast, Dict, DefaultDict, Set, Match, Tuple, List
import sys
from graphviz import Digraph
@@ -16,6 +16,13 @@ from lxml import etree
# Global variables.
tag_type_re = re.compile(r'\{.*\}(.*)')
element_type_re = re.compile(r'.*:(.*)')
primitive_translation_map = {
'string': 'str',
'double': 'float',
'boolean': 'bool',
'long': 'int',
'dateTime': 'datetime',
}
def render_digraph(graph, filename):
@@ -32,8 +39,14 @@ def render_digraph(graph, filename):
g.render()
def primitive_translate(type_str):
# Translate the primitive values, but default to the actual value.
return primitive_translation_map.get(type_str, type_str)
def extract_type(type_str):
return cast(Match, element_type_re.match(type_str)).group(1)
return primitive_translate(
cast(Match, element_type_re.match(type_str)).group(1))
def extract_tag_type(tag_type_str):
@@ -71,15 +84,15 @@ def get_dependencies(xs_el) -> Tuple[Set[str], Dict[str, str]]:
restriction = xs_el.getchildren()[0]
restriction_type = extract_type(restriction.attrib['base'])
if restriction_type == 'string':
if restriction_type == 'str':
restriction_children = restriction.getchildren()
if extract_tag_type(restriction_children[0].tag) == 'enumeration':
type_fields['__inherits__'] = 'Enum'
for rc in restriction_children:
rc_type = rc.attrib['value']
rc_type = primitive_translate(rc.attrib['value'])
type_fields[rc_type] = rc_type
else:
type_fields['__inherits__'] = 'string'
type_fields['__inherits__'] = 'str'
else:
type_fields['__inherits__'] = restriction_type
@@ -139,7 +152,7 @@ def get_dependencies(xs_el) -> Tuple[Set[str], Dict[str, str]]:
else:
raise Exception(f'Unknown tag type {tag_type}.')
depends_on -= {'boolean', 'int', 'string', 'float', 'long', 'dateTime'}
depends_on -= {'bool', 'int', 'str', 'float', 'datetime'}
return depends_on, type_fields
@@ -151,7 +164,7 @@ if len(sys.argv) < 3:
schema_file, output_file = sys.argv[1:]
# First pass, determine who depends on what.
# Determine who depends on what and determine what fields are on each object.
# =============================================================================
with open(schema_file) as f:
tree = etree.parse(f)
@@ -204,22 +217,26 @@ dfs(dependency_graph, 'subsonic-response')
output_order = [x[0] for x in sorted(end_times, key=lambda x: x[1])]
output_order.remove('subsonic-response')
# Second pass, determine the fields on each of the elements and create the code
# accordingly.
# Create the code according to the spec that was generated earlier.
# =============================================================================
def generate_class_for_type(type_name):
# print(type_name, type_fields[type_name])
fields = type_fields[type_name]
is_enum = 'Enum' in fields.get('__inherits__', '')
code = ['', '']
inherits_from = ['APIObject']
for inherit in map(str.strip, fields.get('__inherits__', '').split(',')):
if inherit != '':
inherits_from.append(inherit)
inherits = fields.get('__inherits__', '')
is_enum = 'Enum' in inherits
if inherits:
if inherits in primitive_translation_map.values() or is_enum:
inherits_from.append(inherits)
else:
# Add the fields, we can't directly inherit due to the Diamond
# Problem.
fields.update(type_fields[inherits])
format_str = ' ' + ("{} = '{}'" if is_enum else '{}: {}')
@@ -229,7 +246,9 @@ def generate_class_for_type(type_name):
if key.startswith('__'):
continue
# Uppercase the key if an Enum.
key = key.upper() if is_enum else key
code.append(format_str.format(key, value))
has_properties = True
@@ -251,6 +270,6 @@ with open(output_file, 'w+') as outfile:
'from datetime import datetime',
'from typing import List',
'from enum import Enum',
'from .api_object import APIObject',
'from libremsonic.server.api_object import APIObject',
*map(generate_class_for_type, output_order),
]) + '\n')

View File

@@ -13,6 +13,7 @@ def main():
server = Server('ohea', 'https://airsonic.the-evans.family', 'sumner',
'O}/UieSb[nzZ~l[X1S&zzX1Hi')
print(server.ping())
print(server.get_license())
# app = LibremsonicApp()
# app.run(sys.argv)

View File

@@ -25,19 +25,17 @@ def from_json(cls, data):
annotations: Dict[str, Type] = getattr(cls, '__annotations__', {})
print(type(cls))
# Handle primitive of objects
if data is None:
instance = None
elif cls == str:
elif cls == str or issubclass(cls, str):
instance = data
elif cls == int:
elif cls == int or issubclass(cls, int):
instance = int(data)
elif cls == bool:
elif cls == bool or issubclass(cls, bool):
instance = bool(data)
elif type(cls) == EnumMeta:
instance = cls[data]
instance = cls(data)
elif cls == datetime:
instance = parser.parse(data)

View File

@@ -19,11 +19,14 @@ class APIObject:
def __repr__(self):
if isinstance(self, Enum):
return super().__repr__()
if isinstance(self, str):
return self
annotations: Dict[str, Any] = self.get('__annotations__', {})
typename = type(self).__name__
fieldstr = ' '.join([
f'{field}={getattr(self, field)!r}'
for field in annotations.keys() if hasattr(self, field)
for field in annotations.keys()
if hasattr(self, field) and getattr(self, field) is not None
])
return f'<{typename} {fieldstr}>'

View File

@@ -1,31 +1,37 @@
"""
WARNING: AUTOGENERATED FILE
This file was generated by the api_object_generator.py script. Do
not modify this file directly, rather modify the script or run it on
a new API version.
"""
from datetime import datetime
from typing import List
from enum import Enum
from .api_object import APIObject
from libremsonic.server.api_object import APIObject
class SubsonicError(APIObject):
code: int
message: str
def as_exception(self):
return Exception(f'{self.code}: {self.message}')
class AlbumInfo(APIObject):
notes: List[str]
musicBrainzId: List[str]
lastFmUrl: List[str]
smallImageUrl: List[str]
mediumImageUrl: List[str]
largeImageUrl: List[str]
class License(APIObject):
valid: bool
email: str
licenseExpires: datetime
trialExpires: datetime
class MusicFolder(APIObject):
id: int
name: str
class AverageRating(APIObject, float):
pass
class MediaType(APIObject, Enum):
MUSIC = 'music'
PODCAST = 'podcast'
AUDIOBOOK = 'audiobook'
VIDEO = 'video'
class UserRating(APIObject, int):
pass
@@ -63,19 +69,8 @@ class Child(APIObject):
originalHeight: int
class Album(APIObject):
id: int
name: str
artist: str
artistId: int
coverArt: str
songCount: int
duration: int
created: datetime
year: str
genre: str
song: List[Child]
class AlbumList(APIObject):
album: List[Child]
class AlbumID3(APIObject):
@@ -93,7 +88,12 @@ class AlbumID3(APIObject):
genre: str
class AlbumList2(APIObject):
album: List[AlbumID3]
class AlbumWithSongsID3(APIObject):
song: List[Child]
id: str
name: str
artist: str
@@ -107,8 +107,6 @@ class AlbumWithSongsID3(APIObject):
year: int
genre: str
song: List[Child]
class Artist(APIObject):
id: str
@@ -119,6 +117,25 @@ class Artist(APIObject):
averageRating: AverageRating
class ArtistInfoBase(APIObject):
biography: List[str]
musicBrainzId: List[str]
lastFmUrl: List[str]
smallImageUrl: List[str]
mediumImageUrl: List[str]
largeImageUrl: List[str]
class ArtistInfo(APIObject):
similarArtist: List[Artist]
biography: List[str]
musicBrainzId: List[str]
lastFmUrl: List[str]
smallImageUrl: List[str]
mediumImageUrl: List[str]
largeImageUrl: List[str]
class ArtistID3(APIObject):
id: str
name: str
@@ -128,35 +145,61 @@ class ArtistID3(APIObject):
starred: datetime
class ArtistInfo2(APIObject):
similarArtist: List[ArtistID3]
biography: List[str]
musicBrainzId: List[str]
lastFmUrl: List[str]
smallImageUrl: List[str]
mediumImageUrl: List[str]
largeImageUrl: List[str]
class ArtistWithAlbumsID3(APIObject):
album: List[AlbumID3]
id: str
name: str
coverArt: str
artistImageUrl: str
albumCount: int
starred: datetime
album: List[AlbumID3]
class Index(APIObject):
name: str
artist: List[Artist]
class IndexID3(APIObject):
name: str
artist: List[ArtistID3]
name: str
class Indexes(APIObject):
lastModified: int
class ArtistsID3(APIObject):
index: List[IndexID3]
ignoredArticles: str
index: List[Index]
shortcut: List[Artist]
child: List[Child]
class Bookmark(APIObject):
entry: List[Child]
position: int
username: str
comment: str
created: datetime
changed: datetime
class Bookmarks(APIObject):
bookmark: List[Bookmark]
class ChatMessage(APIObject):
username: str
time: int
message: str
class ChatMessages(APIObject):
chatMessage: List[ChatMessage]
class Directory(APIObject):
child: List[Child]
id: str
parent: str
name: str
@@ -165,7 +208,10 @@ class Directory(APIObject):
averageRating: AverageRating
playCount: int
child: List[Child]
class Error(APIObject):
code: int
message: str
class Genre(APIObject):
@@ -173,28 +219,321 @@ class Genre(APIObject):
albumCount: int
class ArtistInfo(APIObject):
biography: str
musicBrainzId: str
lastFmUrl: str
smallImageUrl: str
mediumImageUrl: str
largeImageUrl: str
similarArtist: List[Artist]
class Genres(APIObject):
genre: List[Genre]
class AlbumInfo(APIObject):
notes: str
musicBrainzId: str
lastFmUrl: str
smallImageUrl: str
mediumImageUrl: str
largeImageUrl: str
class Index(APIObject):
artist: List[Artist]
name: str
class Captions(APIObject):
class Indexes(APIObject):
shortcut: List[Artist]
index: List[Index]
child: List[Child]
lastModified: int
ignoredArticles: str
class InternetRadioStation(APIObject):
id: str
name: str
streamUrl: str
homePageUrl: str
class InternetRadioStations(APIObject):
internetRadioStation: List[InternetRadioStation]
class JukeboxStatus(APIObject):
currentIndex: int
playing: bool
gain: float
position: int
class JukeboxPlaylist(APIObject):
entry: List[Child]
currentIndex: int
playing: bool
gain: float
position: int
class License(APIObject):
valid: bool
email: str
licenseExpires: datetime
trialExpires: datetime
class Lyrics(APIObject):
artist: str
title: str
class MusicFolder(APIObject):
id: int
name: str
class MusicFolders(APIObject):
musicFolder: List[MusicFolder]
class PodcastStatus(APIObject, Enum):
NEW = 'new'
DOWNLOADING = 'downloading'
COMPLETED = 'completed'
ERROR = 'error'
DELETED = 'deleted'
SKIPPED = 'skipped'
class PodcastEpisode(APIObject):
streamId: str
channelId: str
description: str
status: PodcastStatus
publishDate: datetime
id: str
parent: str
isDir: bool
title: str
album: str
artist: str
track: int
year: int
genre: str
coverArt: str
size: int
contentType: str
suffix: str
transcodedContentType: str
transcodedSuffix: str
duration: int
bitRate: int
path: str
isVideo: bool
userRating: UserRating
averageRating: AverageRating
playCount: int
discNumber: int
created: datetime
starred: datetime
albumId: str
artistId: str
type: MediaType
bookmarkPosition: int
originalWidth: int
originalHeight: int
class NewestPodcasts(APIObject):
episode: List[PodcastEpisode]
class NowPlayingEntry(APIObject):
username: str
minutesAgo: int
playerId: int
playerName: str
id: str
parent: str
isDir: bool
title: str
album: str
artist: str
track: int
year: int
genre: str
coverArt: str
size: int
contentType: str
suffix: str
transcodedContentType: str
transcodedSuffix: str
duration: int
bitRate: int
path: str
isVideo: bool
userRating: UserRating
averageRating: AverageRating
playCount: int
discNumber: int
created: datetime
starred: datetime
albumId: str
artistId: str
type: MediaType
bookmarkPosition: int
originalWidth: int
originalHeight: int
class NowPlaying(APIObject):
entry: List[NowPlayingEntry]
class PlayQueue(APIObject):
entry: List[Child]
current: int
position: int
username: str
changed: datetime
changedBy: str
class Playlist(APIObject):
allowedUser: List[str]
id: str
name: str
comment: str
owner: str
public: bool
songCount: int
duration: int
created: datetime
changed: datetime
coverArt: str
class PlaylistWithSongs(APIObject):
entry: List[Child]
allowedUser: List[str]
id: str
name: str
comment: str
owner: str
public: bool
songCount: int
duration: int
created: datetime
changed: datetime
coverArt: str
class Playlists(APIObject):
playlist: List[Playlist]
class PodcastChannel(APIObject):
episode: List[PodcastEpisode]
id: str
url: str
title: str
description: str
coverArt: str
originalImageUrl: str
status: PodcastStatus
errorMessage: str
class Podcasts(APIObject):
channel: List[PodcastChannel]
class ResponseStatus(APIObject, Enum):
OK = 'ok'
FAILED = 'failed'
class ScanStatus(APIObject):
scanning: bool
count: int
class SearchResult(APIObject):
match: List[Child]
offset: int
totalHits: int
class SearchResult2(APIObject):
artist: List[Artist]
album: List[Child]
song: List[Child]
class SearchResult3(APIObject):
artist: List[ArtistID3]
album: List[AlbumID3]
song: List[Child]
class Share(APIObject):
entry: List[Child]
id: str
url: str
description: str
username: str
created: datetime
expires: datetime
lastVisited: datetime
visitCount: int
class Shares(APIObject):
share: List[Share]
class SimilarSongs(APIObject):
song: List[Child]
class SimilarSongs2(APIObject):
song: List[Child]
class Songs(APIObject):
song: List[Child]
class Starred(APIObject):
artist: List[Artist]
album: List[Child]
song: List[Child]
class Starred2(APIObject):
artist: List[ArtistID3]
album: List[AlbumID3]
song: List[Child]
class TopSongs(APIObject):
song: List[Child]
class User(APIObject):
folder: List[int]
username: str
email: str
scrobblingEnabled: bool
maxBitRate: int
adminRole: bool
settingsRole: bool
downloadRole: bool
uploadRole: bool
playlistRole: bool
coverArtRole: bool
commentRole: bool
podcastRole: bool
streamRole: bool
jukeboxRole: bool
shareRole: bool
videoConversionRole: bool
avatarLastChanged: datetime
class Users(APIObject):
user: List[User]
class Version(APIObject, str):
pass
class AudioTrack(APIObject):
@@ -203,6 +542,11 @@ class AudioTrack(APIObject):
languageCode: str
class Captions(APIObject):
id: str
name: str
class VideoConversion(APIObject):
id: str
bitRate: int
@@ -210,96 +554,59 @@ class VideoConversion(APIObject):
class VideoInfo(APIObject):
id: str
captions: List[Captions]
audioTrack: List[AudioTrack]
conversion: List[VideoConversion]
class ArtistsID3(APIObject):
ignoredArticles: str
index: List[IndexID3]
class MusicFolders(APIObject):
musicFolder: List[MusicFolder]
class Genres(APIObject):
genre: List[Genre]
class Artists(APIObject):
index: List[Index]
id: str
class Videos(APIObject):
video: List[Child]
class SimilarSongs(APIObject):
song: List[Child]
class TopSongs(APIObject):
song: List[Child]
class AlbumList(APIObject):
album: List[Album]
class ResponseStatus(APIObject, Enum):
ok = "ok"
failed = "failed"
class SubsonicResponse(APIObject):
# On every Subsonic Response
status: ResponseStatus
version: str
# One of these will exist on each SubsonicResponse
album: AlbumWithSongsID3
albumInfo: AlbumInfo
albumList: AlbumList
albumList2: AlbumList2
artist: ArtistWithAlbumsID3
artistInfo: ArtistInfo
artistInfo2: ArtistInfo2
artists: ArtistsID3
bookmarks: Bookmarks
chatMessages: ChatMessages
directory: Directory
error: Error
genres: Genres
indexes: Indexes
internetRadioStations: InternetRadioStations
jukeboxPlaylist: JukeboxPlaylist
jukeboxStatus: JukeboxStatus
license: License
lyrics: Lyrics
class Response(APIObject):
musicFolders: MusicFolders
newestPodcasts: NewestPodcasts
indexes: Indexes
directory: Directory
genres: Genres
artists: ArtistsID3
artist: ArtistWithAlbumsID3
album: AlbumWithSongsID3
song: Child
videos: Videos
videoInfo: VideoInfo
nowPlaying: NowPlaying
playlist: PlaylistWithSongs
playlists: Playlists
playQueue: PlayQueue
podcasts: Podcasts
randomSongs: Songs
scanStatus: ScanStatus
searchResult: SearchResult
searchResult2: SearchResult2
searchResult3: SearchResult3
shares: Shares
similarSongs: SimilarSongs
similarSongs2: SimilarSongs2
song: Child
playlists: Playlists
playlist: PlaylistWithSongs
jukeboxStatus: JukeboxStatus
jukeboxPlaylist: JukeboxPlaylist
license: License
users: Users
user: User
chatMessages: ChatMessages
albumList: AlbumList
albumList2: AlbumList2
randomSongs: Songs
songsByGenre: Songs
lyrics: Lyrics
podcasts: Podcasts
newestPodcasts: NewestPodcasts
internetRadioStations: InternetRadioStations
bookmarks: Bookmarks
playQueue: PlayQueue
shares: Shares
starred: Starred
starred2: Starred2
albumInfo: AlbumInfo
artistInfo: ArtistInfo
artistInfo2: ArtistInfo2
similarSongs: SimilarSongs
similarSongs2: SimilarSongs2
topSongs: TopSongs
user: User
users: Users
videos: Videos
videoInfo: VideoInfo
scanStatus: ScanStatus
error: Error
status: ResponseStatus
version: Version

View File

@@ -1,9 +1,9 @@
import requests
from typing import List, Optional, Dict
from .api_objects import (SubsonicResponse, License, MusicFolder, Indexes,
AlbumInfo, ArtistInfo, VideoInfo, Child, Album,
Artist, Artists, Directory, Genre)
from .api_objects import (Response, License, MusicFolder, Indexes, AlbumInfo,
ArtistInfo, VideoInfo, Child, AlbumID3, Artist,
ArtistsID3, Directory, Genre)
class Server:
@@ -29,12 +29,12 @@ class Server:
def _make_url(self, endpoint: str) -> str:
return f'{self.hostname}/rest/{endpoint}.view'
def _post(self, url, **params) -> SubsonicResponse:
def _post(self, url, **params) -> Response:
"""
Make a post to a *Sonic REST API. Handle all types of errors including
*Sonic ``<error>`` responses.
:returns: a SubsonicResponse containing all of the data of the
:returns: a Response containing all of the data of the
response, deserialized
:raises Exception: needs some work TODO
"""
@@ -54,7 +54,7 @@ class Server:
# TODO: logging
print(subsonic_response)
response = SubsonicResponse.from_json(subsonic_response)
response = Response.from_json(subsonic_response)
# Check for an error and if it exists, raise it.
if response.get('error'):
@@ -62,7 +62,7 @@ class Server:
return response
def ping(self) -> SubsonicResponse:
def ping(self) -> Response:
"""
Used to test connectivity with the server.
"""
@@ -73,7 +73,6 @@ class Server:
Get details about the software license.
"""
result = self._post(self._make_url('getLicense'))
print(result)
return result.license
def get_music_folders(self) -> List[MusicFolder]:
@@ -119,7 +118,7 @@ class Server:
result = self._post(self._make_url('getGenres'))
return result.genres.genre
def get_artists(self, music_folder_id: int = None) -> Artists:
def get_artists(self, music_folder_id: int = None) -> ArtistsID3:
"""
Similar to getIndexes, but organizes music according to ID3 tags.
@@ -140,7 +139,7 @@ class Server:
result = self._post(self._make_url('getArtist'), id=artist_id)
return result.artist
def get_album(self, album_id: int) -> Album:
def get_album(self, album_id: int) -> AlbumID3:
"""
Returns details for an album, including a list of songs. This method
organizes music according to ID3 tags.