From 070773be5d006ef8764f81c075fcc3f37a5ba4e7 Mon Sep 17 00:00:00 2001 From: Marco Yuen Date: Fri, 21 May 2010 10:03:17 -0700 Subject: [PATCH 1/1] Initial commit. --- .gitignore | 1 + Configure.py | 90 ++++++++++++ Identities.py | 17 +++ Info.py | 20 +++ OpenCirrus.py | 17 +++ PlanetLab.py | 31 ++++ SfaBrowser.py | 17 +++ SfaData.py | 68 +++++++++ SfaGUI.py | 111 ++++++++++++++ Sink.py | 36 +++++ SinkList.py | 41 ++++++ Slices.py | 17 +++ VINI.py | 46 ++++++ build.sh | 2 + public/SfaGUI.css | 292 +++++++++++++++++++++++++++++++++++++ public/SfaGUI.html | 13 ++ public/SplitPanel.css | 16 ++ public/images/hborder.png | Bin 0 -> 357 bytes public/images/vborder.png | Bin 0 -> 190 bytes public/splitPanelThumb.png | Bin 0 -> 308 bytes 20 files changed, 835 insertions(+) create mode 100644 .gitignore create mode 100644 Configure.py create mode 100644 Identities.py create mode 100644 Info.py create mode 100644 OpenCirrus.py create mode 100644 PlanetLab.py create mode 100644 SfaBrowser.py create mode 100644 SfaData.py create mode 100644 SfaGUI.py create mode 100644 Sink.py create mode 100644 SinkList.py create mode 100644 Slices.py create mode 100644 VINI.py create mode 100755 build.sh create mode 100644 public/SfaGUI.css create mode 100644 public/SfaGUI.html create mode 100644 public/SplitPanel.css create mode 100644 public/images/hborder.png create mode 100644 public/images/vborder.png create mode 100644 public/splitPanelThumb.png diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea1472e --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +output/ diff --git a/Configure.py b/Configure.py new file mode 100644 index 0000000..ef7fd76 --- /dev/null +++ b/Configure.py @@ -0,0 +1,90 @@ +from Sink import Sink, SinkInfo +from pyjamas.ui.Composite import Composite +from pyjamas.ui.RootPanel import RootPanel +from pyjamas.ui.VerticalPanel import VerticalPanel +from pyjamas.ui.HorizontalPanel import HorizontalPanel +from pyjamas.ui.CaptionPanel import CaptionPanel +from pyjamas.ui.TextBox import TextBox +from pyjamas.ui.Button import Button +from pyjamas.ui.HTML import HTML +from pyjamas.ui.Grid import Grid +from pyjamas.ui import HasAlignment +from SfaData import SfaData + +class TopPanel(Composite): + panel = None + + def __init__(self): + TopPanel.panel = self + Composite.__init__(self) + self.data = SfaData() + + self.outer = HorizontalPanel() + self.inner = VerticalPanel() + + self.outer.setHorizontalAlignment(HasAlignment.ALIGN_RIGHT) + self.inner.setHorizontalAlignment(HasAlignment.ALIGN_RIGHT) + + # No links right now... + self.links = HorizontalPanel() + self.links.setSpacing(4) + + self.outer.add(self.inner) + self.refresh() + + self.initWidget(self.outer) + + def onClick(self, sender): + pass + + def refresh(self): + self.inner.clear() + self.inner.add(HTML("User: %s" % self.data.getUser())) + self.inner.add(HTML("Slice: %s" % self.data.getSlice())) + self.inner.add(self.links) + +class LabeledTextBox: + def __init__(self, label, initialText): + self.hp = CaptionPanel(label) + self.tb = TextBox() + self.tb.setWidth("100%") + self.tb.setText(initialText) + self.hp.add(self.tb) + + def getText(self): + return self.tb.getText() + + def getWidget(self): + return self.hp + +class Configure(Sink): + def __init__(self): + Sink.__init__(self) + self.data = SfaData() + + panel = VerticalPanel() + panel.setWidth("100%") + panel.setWidth("100%") + + self.userBox = LabeledTextBox("User HRN:", SfaData.user) + panel.add(self.userBox.getWidget()) + self.sliceBox = LabeledTextBox("Slice HRN:", SfaData.slice) + panel.add(self.sliceBox.getWidget()) + + hp = HorizontalPanel() + applyButton = Button("Apply", self.apply) + hp.add(applyButton) + panel.add(hp) + + self.initWidget(panel) + + def apply(self, sender): + self.data.setUser(self.userBox.getText()) + self.data.setSlice(self.sliceBox.getText()) + TopPanel.panel.refresh() + + def onShow(self): + pass + +def init(): + return SinkInfo("Configure", "Configure SFA Federation GUI", Configure) diff --git a/Identities.py b/Identities.py new file mode 100644 index 0000000..a6a6f39 --- /dev/null +++ b/Identities.py @@ -0,0 +1,17 @@ +from Sink import Sink, SinkInfo +from pyjamas.ui.HTML import HTML + +class Identities(Sink): + def __init__(self): + + Sink.__init__(self) + + text="Not implemented yet" + self.initWidget(HTML(text, True)) + + def onShow(self): + pass + + +def init(): + return SinkInfo("Identities", "Manage SFA Identities", Identities) diff --git a/Info.py b/Info.py new file mode 100644 index 0000000..4fcf72a --- /dev/null +++ b/Info.py @@ -0,0 +1,20 @@ +from Sink import Sink, SinkInfo +from pyjamas.ui.HTML import HTML + +class Info(Sink): + def __init__(self): + + Sink.__init__(self) + + text="
This is the SFA Federation GUI. " + text+="It allows users to manage their SFA identities and slices, " + text+="add resources from various testbeds to their slices, " + text+="and browse the SFA Registry.

" + self.initWidget(HTML(text, True)) + + def onShow(self): + pass + + +def init(): + return SinkInfo("Info", "Introduction to the SFA Federation GUI", Info) diff --git a/OpenCirrus.py b/OpenCirrus.py new file mode 100644 index 0000000..537f20b --- /dev/null +++ b/OpenCirrus.py @@ -0,0 +1,17 @@ +from Sink import Sink, SinkInfo +from pyjamas.ui.HTML import HTML + +class OpenCirrus(Sink): + def __init__(self): + + Sink.__init__(self) + + text="Not implemented yet" + self.initWidget(HTML(text, True)) + + def onShow(self): + pass + + +def init(): + return SinkInfo("OpenCirrus", "Specify OpenCirrus Resources", OpenCirrus) diff --git a/PlanetLab.py b/PlanetLab.py new file mode 100644 index 0000000..50407c7 --- /dev/null +++ b/PlanetLab.py @@ -0,0 +1,31 @@ +from Sink import Sink, SinkInfo +from pyjamas.ui.VerticalPanel import VerticalPanel +from pyjamas.ui.TextArea import TextArea +from pyjamas.ui.HTML import HTML +from SfaData import PlanetLabData + +class PlanetLab(Sink): + def __init__(self): + + Sink.__init__(self) + self.panel = VerticalPanel() + self.panel.setSize("100%", "100%") + self.data = PlanetLabData() + self.rspec = self.data.getRSpec() + + # Just to show that we can retrieve the RSpec + ta = TextArea() + ta.setSize("100%", "100%") + ta.setText(self.rspec) + self.panel.add(ta) + + self.initWidget(self.panel) + + def onShow(self): + # Do we want to refresh the RSpec? + pass + + + +def init(): + return SinkInfo("PlanetLab", "Specify PlanetLab Resources", PlanetLab) diff --git a/SfaBrowser.py b/SfaBrowser.py new file mode 100644 index 0000000..778b448 --- /dev/null +++ b/SfaBrowser.py @@ -0,0 +1,17 @@ +from Sink import Sink, SinkInfo +from pyjamas.ui.HTML import HTML + +class SfaBrowser(Sink): + def __init__(self): + + Sink.__init__(self) + + text="Not implemented yet" + self.initWidget(HTML(text, True)) + + def onShow(self): + pass + + +def init(): + return SinkInfo("Browse SFA", "SFA Hierarchy Browser", SfaBrowser) diff --git a/SfaData.py b/SfaData.py new file mode 100644 index 0000000..a26aaf9 --- /dev/null +++ b/SfaData.py @@ -0,0 +1,68 @@ +import os +from subprocess import call +from sfa.util.rspecHelper import RSpec + +class SfaData: + authority = "plc.princeton" + user = "plc.princeton.acb" + slice = "plc.princeton.iias" + + def __init__(self): + self.registry = None + self.slicemgr = None + + def getAuthority(self): + return SfaData.authority + + def getUser(self): + return SfaData.user + + def setUser(self, user): + SfaData.user = user + + # Should probably get authority from user record instead... + a = SfaData.user.split('.') + SfaData.authority = '.'.join(a[:len(a)-1]) + + def getSlice(self): + return SfaData.slice + + def setSlice(self, slice): + SfaData.slice = slice + + def getRecord(self): + pass + + def getRSpec(self): + slice = self.getSlice() + call(["sfi.py", "-u", self.getUser(), "-a", self.getAuthority(), + "-r", self.registry, "-s", self.slicemgr, "resources", + "-o", slice, slice]) + filename = os.path.expanduser("~/.sfi/" + slice + ".rspec") + f = open(filename, "r") + xml = f.read() + f.close() + return xml + +class ViniData(SfaData): + def __init__(self): + SfaData.__init__(self) + self.registry = "http://www.planet-lab.org:12345" + self.slicemgr = "http://www.vini-veritas.net:12347" + + def getRSpec(self): + xml = SfaData.getRSpec(self) + return RSpec(xml) + +class PlanetLabData(SfaData): + def __init__(self): + SfaData.__init__(self) + self.registry = "http://www.planet-lab.org:12345" + self.slicemgr = "http://www.planet-lab.org:12347" + +class OpenCirrusData(SfaData): + def __init__(self): + SfaData.__init__(self) + self.registry = "http://www.planet-lab.org:12345" + self.slicemgr = "http://www.planet-lab.org:12347" + diff --git a/SfaGUI.py b/SfaGUI.py new file mode 100644 index 0000000..8cd7cc1 --- /dev/null +++ b/SfaGUI.py @@ -0,0 +1,111 @@ +import pyjd # this is dummy in pyjs + +from pyjamas.ui.Button import Button +from pyjamas.ui.RootPanel import RootPanel +from pyjamas.ui.HTML import HTML +from pyjamas.ui.DockPanel import DockPanel +from pyjamas.ui import HasAlignment +from pyjamas.ui.Hyperlink import Hyperlink +from pyjamas.ui.VerticalPanel import VerticalPanel +from pyjamas import Window +from SinkList import SinkList +from Configure import TopPanel +from pyjamas import History +import Info +import Slices +import Identities +import PlanetLab +import VINI +import OpenCirrus +import SfaBrowser +import Configure + +class SfaGUI: + def onHistoryChanged(self, token): + info = self.sink_list.find(token) + if info: + self.show(info, False) + else: + self.showInfo() + + def onModuleLoad(self): + self.tp = TopPanel() + self.tp.setWidth("100%") + + self.curInfo='' + self.curSink=None + self.description=HTML() + self.sink_list=SinkList() + self.panel=DockPanel() + self.loadSinks() + self.sinkContainer = DockPanel() + self.sinkContainer.setStyleName("ks-Sink") + + vp=VerticalPanel() + vp.setWidth("100%") + vp.setHeight("100%") + vp.add(self.description) + vp.add(self.sinkContainer) + + self.description.setStyleName("ks-Info") + + self.panel.add(self.sink_list, DockPanel.WEST) + self.panel.add(vp, DockPanel.CENTER) + + self.panel.setCellVerticalAlignment(self.sink_list, HasAlignment.ALIGN_TOP) + self.panel.setCellWidth(vp, "100%") + self.panel.setCellHeight(vp, "100%") + + History.addHistoryListener(self) + RootPanel().add(self.tp) + RootPanel().add(self.panel) + + #Show the initial screen. + initToken = History.getToken() + if len(initToken): + self.onHistoryChanged(initToken) + else: + self.showInfo() + + def show(self, info, affectHistory): + if info == self.curInfo: return + self.curInfo = info + + if self.curSink <> None: + self.curSink.onHide() + self.sinkContainer.remove(self.curSink) + + self.curSink = info.getInstance() + self.sink_list.setSinkSelection(info.getName()) + self.description.setHTML(info.getDescription()) + + if (affectHistory): + History.newItem(info.getName()) + + self.sinkContainer.add(self.curSink, DockPanel.CENTER) + self.sinkContainer.setCellWidth(self.curSink, "100%") + self.sinkContainer.setCellHeight(self.curSink, "100%") + self.sinkContainer.setCellVerticalAlignment(self.curSink, HasAlignment.ALIGN_TOP) + self.curSink.onShow() + + def loadSinks(self): + self.sink_list.addSink(Info.init()) + self.sink_list.addSink(Identities.init()) + self.sink_list.addSink(Slices.init()) + self.sink_list.addSink(PlanetLab.init()) + self.sink_list.addSink(VINI.init()) + self.sink_list.addSink(OpenCirrus.init()) + self.sink_list.addSink(SfaBrowser.init()) + self.sink_list.addSink(Configure.init()) + + def showInfo(self): + self.show(self.sink_list.find("Info"), False) + + + + +if __name__ == '__main__': + pyjd.setup("public/SfaGUI.html") + app = SfaGUI() + app.onModuleLoad() + pyjd.run() diff --git a/Sink.py b/Sink.py new file mode 100644 index 0000000..8032d90 --- /dev/null +++ b/Sink.py @@ -0,0 +1,36 @@ +from pyjamas.ui.Composite import Composite + +class Sink(Composite): + def __init__(self): + Composite.__init__(self) + + def onHide(self): + pass + + def onShow(self): + pass + + def baseURL(self): + return "" + +class SinkInfo: + def __init__(self, name, desc, object_type): + self.name=name + self.description=desc + self.object_type=object_type + self.instance=None + + def createInstance(self): + return self.object_type() + + def getDescription(self): + return self.description + + def getInstance(self): + if self.instance==None: + self.instance=self.createInstance() + return self.instance + + def getName(self): + return self.name + diff --git a/SinkList.py b/SinkList.py new file mode 100644 index 0000000..129a26f --- /dev/null +++ b/SinkList.py @@ -0,0 +1,41 @@ +from pyjamas.ui.Composite import Composite +from pyjamas.ui.VerticalPanel import VerticalPanel +from pyjamas.ui.Hyperlink import Hyperlink + +class SinkList(Composite): + def __init__(self): + Composite.__init__(self) + + self.vp_list=VerticalPanel() + self.sinks=[] + self.selectedSink=-1 + + self.initWidget(self.vp_list) + self.setStyleName("ks-List") + + def addSink(self, info): + name = info.getName() + link = Hyperlink(name, False, name) + link.setStyleName("ks-SinkItem") + self.vp_list.add(link) + self.sinks.append(info) + + def find(self, sinkName): + for info in self.sinks: + if info.getName()==sinkName: + return info + return None + + def setSinkSelection(self, name): + if self.selectedSink <> -1: + self.vp_list.getWidget(self.selectedSink).removeStyleName("ks-SinkItem-selected") + + for i in range(len(self.sinks)): + info = self.sinks[i] + if (info.getName()==name): + self.selectedSink = i + widget=self.vp_list.getWidget(self.selectedSink) + widget.addStyleName("ks-SinkItem-selected") + return + + diff --git a/Slices.py b/Slices.py new file mode 100644 index 0000000..706e8b7 --- /dev/null +++ b/Slices.py @@ -0,0 +1,17 @@ +from Sink import Sink, SinkInfo +from pyjamas.ui.HTML import HTML + +class Slices(Sink): + def __init__(self): + + Sink.__init__(self) + + text="Not implemented yet" + self.initWidget(HTML(text, True)) + + def onShow(self): + pass + + +def init(): + return SinkInfo("Slices", "Manage SFA Slices", Slices) diff --git a/VINI.py b/VINI.py new file mode 100644 index 0000000..c82c7fd --- /dev/null +++ b/VINI.py @@ -0,0 +1,46 @@ +from Sink import Sink, SinkInfo +from pyjamas.ui.horizsplitpanel import HorizontalSplitPanel +from pyjamas.ui.CaptionPanel import CaptionPanel +from pyjamas.ui.ListBox import ListBox +from pyjamas.ui.HTML import HTML +from SfaData import ViniData + +class VINI(Sink): + def __init__(self): + + Sink.__init__(self) + self.panel = HorizontalSplitPanel() + self.panel.setSize("100%", "100%") + self.panel.setSplitPosition("50%") + self.data = ViniData() + self.rspec = self.data.getRSpec() + + leftcap = CaptionPanel("Available nodes") + leftcap.setSize("90%", "90%") + leftlist = ListBox(MultipleSelect=True) + leftlist.setSize("100%", "100%") + available = self.rspec.get_node_list() + for i in available: + leftlist.addItem(i) + leftcap.add(leftlist) + + rightcap = CaptionPanel("Selected nodes") + rightcap.setSize("90%", "90%") + rightlist = ListBox(MultipleSelect=True) + rightlist.setSize("100%", "100%") + slivers = self.rspec.get_sliver_list() + for i in slivers: + rightlist.addItem(i) + rightcap.add(rightlist) + + self.panel.setLeftWidget(leftcap) + self.panel.setRightWidget(rightcap) + + self.initWidget(self.panel) + + def onShow(self): + pass + + +def init(): + return SinkInfo("VINI", "Specify VINI Resources", VINI) diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..b29cee9 --- /dev/null +++ b/build.sh @@ -0,0 +1,2 @@ +#!/bin/sh +../../bin/pyjsbuild $@ SfaGUI diff --git a/public/SfaGUI.css b/public/SfaGUI.css new file mode 100644 index 0000000..5c3f69c --- /dev/null +++ b/public/SfaGUI.css @@ -0,0 +1,292 @@ +body { + background-color: white; + color: black; + font-family: Arial, sans-serif; + font-size: smaller; + margin: 20px 20px 20px 20px; +} + +code { + font-size: small; +} + +a { + color: darkblue; +} + +a:visited { + color: darkblue; +} + +.gwt-BorderedPanel { +} + +.gwt-Button { +} + +.gwt-Canvas { +} + +.gwt-CheckBox { + font-size: smaller; +} + +.gwt-DialogBox { + sborder: 8px solid #C3D9FF; + border: 2px outset; + background-color: white; +} + +.gwt-DialogBox .Caption { + background-color: #C3D9FF; + padding: 3px; + margin: 2px; + font-weight: bold; + cursor: default; +} + +.gwt-FileUpload { +} + +.gwt-Frame { +} + +.gwt-HorizontalSplitter .Bar { + width: 8px; + background-color: #C3D9FF; +} + +.gwt-VerticalSplitter .Bar { + height: 8px; + background-color: #C3D9FF; +} + +.gwt-HTML { + font-size: smaller; +} + +.gwt-Hyperlink { +} + +.gwt-Image { +} + +.gwt-Label { + font-size: smaller; +} + +.gwt-ListBox { +} + +.gwt-MenuBar { + background-color: #C3D9FF; + border: 1px solid #87B3FF; + cursor: default; +} + +.gwt-MenuBar .gwt-MenuItem { + padding: 1px 4px 1px 4px; + font-size: smaller; + cursor: default; +} + +.gwt-MenuBar .gwt-MenuItem-selected { + background-color: #E8EEF7; +} + +.gwt-PasswordTextBox { +} + +.gwt-RadioButton { + font-size: smaller; +} + +.gwt-TabPanel { +} + +.gwt-TabPanelBottom { + border-left: 1px solid #87B3FF; +} + +.gwt-TabBar { + background-color: #C3D9FF; + font-size: smaller; +} + +.gwt-TabBar .gwt-TabBarFirst { + height: 100%; + border-bottom: 1px solid #87B3FF; + padding-left: 3px; +} + +.gwt-TabBar .gwt-TabBarRest { + border-bottom: 1px solid #87B3FF; + padding-right: 3px; +} + +.gwt-TabBar .gwt-TabBarItem { + border-top: 1px solid #C3D9FF; + border-bottom: 1px solid #87B3FF; + padding: 2px; + cursor: pointer; + cursor: hand; +} + +.gwt-TabBar .gwt-TabBarItem-selected { + font-weight: bold; + background-color: #E8EEF7; + border-top: 1px solid #87B3FF; + border-left: 1px solid #87B3FF; + border-right: 1px solid #87B3FF; + border-bottom: 1px solid #E8EEF7; + padding: 2px; + cursor: default; +} + +.gwt-TextArea { +} + +.gwt-TextBox { +} + +.gwt-Tree { +} + +.gwt-Tree .gwt-TreeItem { + font-size: smaller; +} + +.gwt-Tree .gwt-TreeItem-selected { + background-color: #C3D9FF; +} + +.gwt-StackPanel { +} + +.gwt-StackPanel .gwt-StackPanelItem { + background-color: #C3D9FF; + cursor: pointer; + cursor: hand; +} + +.gwt-StackPanel .gwt-StackPanelItem-selected { +} + +/* -------------------------------------------------------------------------- */ +.ks-Sink { + border: 8px solid #C3D9FF; + background-color: #E8EEF7; + width: 100%; + height: 24em; +} + +.ks-Info { + background-color: #C3D9FF; + padding: 10px 10px 2px 10px; + font-size: smaller; +} + +.ks-List { + margin-top: 8px; + margin-bottom: 8px; + font-size: smaller; +} + +.ks-List .ks-SinkItem { + width: 100%; + padding: 0.3em; + padding-right: 16px; + cursor: pointer; + cursor: hand; +} + +.ks-List .ks-SinkItem-selected { + background-color: #C3D9FF; +} + +.ks-images-Image { + margin: 8px; +} + +.ks-images-Button { + margin: 8px; + cursor: pointer; + cursor: hand; +} + +.ks-layouts { + margin: 8px; +} + +.ks-layouts-Label { + background-color: #C3D9FF; + font-weight: bold; + margin-top: 1em; + padding: 2px 0px 2px 0px; + width: 100%; +} + +.ks-layouts-Scroller { + height: 128px; + border: 2px solid #C3D9FF; + padding: 8px; + margin: 8px; +} + +.ks-popups-Popup { + background-color: white; + border: 1px solid #87B3FF; + padding: 4px; +} + +.infoProse { + margin: 8px; +} + +/****************************************************** +* DisclosurePanel +******************************************************/ +.gwt-DisclosurePanel +{ + background-color : #ddf; + border : 1px solid #009; +} +.gwt-DisclosurePanel .header +{ + font-size : 70%; + cursor : pointer; + cursor : hand; +} +.gwt-DisclosurePanel-closed +{ +} +.gwt-DisclosurePanel-closed .header +{ +} +.gwt-DisclosurePanel-open +{ +} +.gwt-DisclosurePanel-open .header +{ +} +.gwt-DisclosurePanel-open .content +{ +} + +.gwt-HorizontalSplitPanel { +} + +.gwt-HorizontalSplitPanel .hsplitter { + cursor: move; + border: 0px; + background: #91c0ef url(images/vborder.png) repeat-x; +} + +.gwt-VerticalSplitPanel { +} + +.gwt-VerticalSplitPanel .vsplitter { + cursor: move; + border: 0px; + background: #91c0ef url(images/hborder.png) repeat-x; +} diff --git a/public/SfaGUI.html b/public/SfaGUI.html new file mode 100644 index 0000000..d50c334 --- /dev/null +++ b/public/SfaGUI.html @@ -0,0 +1,13 @@ + + + + + SFA Federation GUI + + + + + + + + diff --git a/public/SplitPanel.css b/public/SplitPanel.css new file mode 100644 index 0000000..4706eb7 --- /dev/null +++ b/public/SplitPanel.css @@ -0,0 +1,16 @@ +.gwt-HorizontalSplitPanel { +} + +.gwt-HorizontalSplitPanel .hsplitter { + cursor: move; + border: 0px; + background: #91c0ef url(images/vborder.png) repeat-x; +} +.gwt-VerticalSplitPanel { +} + +.gwt-VerticalSplitPanel .vsplitter { + cursor: move; + border: 0px; + background: #91c0ef url(images/hborder.png) repeat-x; +} diff --git a/public/images/hborder.png b/public/images/hborder.png new file mode 100644 index 0000000000000000000000000000000000000000..509eb1bab153910a0debe3958432e7475aecb896 GIT binary patch literal 357 zcmeAS@N?(olHy`uVBq!ia0vp^j0_B%_c_>rtVKKjumUNzByV>YhCLvt^nBS)pa^Gy zM`SSr1Gg{;GcwGYBLNg-FY)wsWxv59Afmx@>m+_rU><*`q5xtIyW` yulYA4+}jK&(_>igH}T6hxx(z*+s-L(p8va+_uX5KKQ2JuGkCiCxvXx@Qfx`y?k)`fL2$v|<&%LToCO|{ z#S9GG!XV7ZFl&wkP>{XE)7O>#2CEFe0o&d8?S4QZZ%-G;5Rc=@dp|3k*F{R#`7rEL_~yhr|%#VP)7F WEVyGrjY>%7y|+VqilhJXvmQ~s#EIV@K73FEn#6RHkB^TNBq1&?&H!XH{{H=&O;}iXCnLx~g@uLtfr9!#{QJ|V zPpm>hLKE}z^BcjEAkfj#Vb8_Ir2%xq;mpj;YalUz00RJZxI%?M`nN^^0000