I am working with FluentCassandra in F# and attempting to convert a string to a UTF8Type in order to use the ExecuteNonQuery method. Has anyone been successful doing this?
Thanks,
Tom
Thank you Jack P. and Daniel for pointing me in the right direction.
To provide more examples so others can benefit, I am writing a wrapper on top of FluentCassandra in F# to make CRUD functionality much simpler by utilizing the succinctness of F#. I am using Nick Berardi's code as an example for this wrapper:
https://github.com/fluentcassandra/fluentcassandra/blob/master/test/FluentCassandra.Sandbox/Program.cs
For example, if you want to check if a keyspace exists, simply calling the KeySpaceExists(keyspaceName) would allow for checking if a keyspace exists, using CreateKeyspace(keyspaceName) would allow for creation of a keyspace, etc. An example of the library I am creating is here:
namespace Test
open System
open System.Collections.Generic
open System.Configuration
open System.Linq
open System.Text
open System.Windows
open FluentCassandra
open FluentCassandra.Connections
open FluentCassandra.Types
open FluentCassandra.Linq
module Cassandra =
let GetAppSettings (key : string) = ConfigurationManager.AppSettings.Item(key)
let KeyspaceExists keyspaceName =
let server = new Server(GetAppSettings("Server"))
let db = new CassandraContext(keyspaceName, server)
let keyspaceNameExists = db.KeyspaceExists(keyspaceName)
db.Dispose()
keyspaceNameExists
let CreateKeyspace keyspaceName =
let server = new Server(GetAppSettings("Server"))
let db = new CassandraContext(keyspaceName, server)
let schema = new CassandraKeyspaceSchema(Name=keyspaceName)
let keyspace = new CassandraKeyspace(schema,db)
if KeyspaceExists(keyspaceName)=false then keyspace.TryCreateSelf()
db.Dispose()
let DropKeyspace (keyspaceName : string ) =
let server = new Server(GetAppSettings("Server"))
let db = new CassandraContext(keyspaceName, server)
match db.KeyspaceExists(keyspaceName)=true with
// value has "ignore" to ignore the string returned from FluentCassandra
| true -> db.DropKeyspace(keyspaceName) |> ignore
| _ -> ()
db.Dispose()
let ColumnFamilyExists (keyspaceName, columnFamilyName : string) =
let server = new Server(GetAppSettings("Server"))
let db = new CassandraContext(keyspaceName, server)
let schema = new CassandraKeyspaceSchema(Name=keyspaceName)
let keyspace = new CassandraKeyspace(schema,db)
let columnFamilyNameExists = db.ColumnFamilyExists(columnFamilyName)
db.Dispose()
columnFamilyNameExists
let CreateColumnFamily (keyspaceName, columnFamilyName: string) =
if ColumnFamilyExists(keyspaceName,columnFamilyName)=false then
let server = new Server(GetAppSettings("Server"))
let db = new CassandraContext(keyspaceName, server)
let schema = new CassandraKeyspaceSchema(Name=keyspaceName)
let keyspace = new CassandraKeyspace(schema,db)
if ColumnFamilyExists(keyspaceName,columnFamilyName)=false then
keyspace.TryCreateColumnFamily(new CassandraColumnFamilySchema(FamilyName = columnFamilyName, KeyValueType = CassandraType.AsciiType, ColumnNameType = CassandraType.IntegerType, DefaultColumnValueType = CassandraType.UTF8Type))
let ExecuteNonQuery(keyspaceName, query: string) =
let server = new Server(GetAppSettings("Server"))
let db = new CassandraContext(keyspaceName, server)
let schema = new CassandraKeyspaceSchema(Name=keyspaceName)
let keyspace = new CassandraKeyspace(schema,db)
let queryUTF8 = FluentCassandra.Types.UTF8Type.op_Implicit query
try
db.ExecuteNonQuery(queryUTF8)
true
with
| _ -> false
This library allows for very easy one line commands to utilize the FluentCassandra functionality. Of course this is just the start and I plan on amending the above library further.
open System
open System.Linq
open System.Collections.Generic
open System.Configuration
open FluentCassandra.Connections
open FluentCassandra.Types
open FluentCassandra.Linq
open Test.Cassandra
[<EntryPoint>]
let main argv =
CreateKeyspace("test1")
printfn "%s" (ColumnFamilyExists("test1", "table1").ToString())
printfn "%s" (KeyspaceExists("test1").ToString())
CreateColumnFamily("test1","table1")
printfn "%s" (ColumnFamilyExists("test1", "table1").ToString())
let result = ExecuteNonQuery("test1", "CREATE TABLE table2 (id bigint primary key, name varchar)")
printfn "%s" (result.ToString())
let wait = System.Console.ReadLine()
0
Specifically with converting the query string to a UTF8Type, Daniel's approach of utilizing UTF8Type.op_Implicit str worked. You can see how I applied it in the ExecuteNonQuery function above. Thanks again for your help!
Related
I have the following F# code
open FSharp.Data.Sql
open FSharp.Data.Sql.Runtime
open System.IO
[<Literal>]
let private schemaConn = #"Driver={Microsoft dBASE Driver (*.dbf)};DriverID=277;Dbq=C:\Citect\User\NPM;"
type private schema = SqlDataProvider<Common.DatabaseProviderTypes.ODBC, schemaConn>
let private connStringFormat = Printf.StringFormat<string->string>(#"Driver={Microsoft dBASE Driver (*.dbf)};DriverID=277;Dbq=%s;")
type internal Project = {
name : string
path : string
dcx : schema.dataContext
}
[<Literal>]
let private cUserPath = #"C:\Citect\User"
let private findPath projectName =
Directory.GetDirectories(cUserPath, projectName, SearchOption.AllDirectories)
|> Array.find (fun d -> d.Contains("web") |> not)
let internal connect projectName =
let path' = findPath projectName
let connString = sprintf connStringFormat path'
let dcx' = schema.GetDataContext(connString)
{ name = projectName; path = path'; dcx = dcx' }
let internal updVariable (project : Project) variable =
let dcx = project.dcx
let q = query {
for v in dcx.Dbo.Variable do
where (v.Addr = "%MW217.0")
select v
exactlyOne
}
q.Addr <- "QQQ"
dcx.SubmitUpdates() //error
let internal prj = connect "NPMUG_SCC35"
updVariable prj ()
Connection and query work as expected, but when I try to update the data source I get the following error coming from the odbc driver:
Message -> ERROR [HY092] [Microsoft][ODBC dBase Driver]Invalid
attribute/option identifier Source -> odbcjt32.dll
Is there a way to get it working or do I need to give up the type provider and resort back to OleDb?
UPDATE
Disabling transactions makes things a little better, now the error is due to the missing primary key in the dbf files I have to work with.
The only code changed is getting the data context
let dcx = schema.GetDataContext( { Timeout = TimeSpan.MaxValue; IsolationLevel = Transactions.IsolationLevel.DontCreateTransaction } : FSharp.Data.Sql.Transactions.TransactionOptions)
And the new error is:
System.Exception: Error - you cannot update an entity that does not
have a primary key. (dbo.variable) at
FSharp.Data.Sql.Providers.OdbcProvider.createUpdateCommand(IDbConnection
con, StringBuilder sb, SqlEntity entity, FSharpList`1 changedColumns)
at .$Providers.Odbc.FSharp-Data-Sql-Common-ISqlProvider-ProcessUpdates#648-4.Invoke(SqlEntity
e) at
Microsoft.FSharp.Collections.SeqModule.Iterate[T](FSharpFunc2 action,
IEnumerable1 source) at
FSharp.Data.Sql.Providers.OdbcProvider.FSharp-Data-Sql-Common-ISqlProvider-ProcessUpdates(IDbConnection
con, ConcurrentDictionary2 entities, TransactionOptions
transactionOptions, FSharpOption1 timeout) at
.$SqlRuntime.DataContext.f#1-69(SqlDataContext
__, IDbConnection con, Unit unitVar0) at FSharp.Data.Sql.Runtime.SqlDataContext.FSharp-Data-Sql-Common-ISqlDataContext-SubmitPendingChanges()
Any idea on how to deal with this probem?
I found a tricky/dirty way that I would classify more as a workaround than a real solution, but it works in my case; so I am going to use it unless/until someone else suggests a conclusive one.
To get the type provider working I need to do 2 things not in the usual workflow:
The data context needs to be retrieved with transactions disabled
Before performing changing operations on a DBF, I create a primary
key on that DBF using a lower level SQL statement
Here the working code
[<Literal>]
let private schemaConn = #"Driver={Microsoft dBASE Driver (*.dbf)};DriverID=277;Dbq=C:\Citect\User\NPM;READONLY=FALSE"
type private schema = SqlDataProvider<Common.DatabaseProviderTypes.ODBC, schemaConn>
let private connStringFormat = Printf.StringFormat<string->string>(#"Driver={Microsoft dBASE Driver (*.dbf)};DriverID=277;Dbq=%s;READONLY=FALSE")
type internal Project = {
name : string
path : string
dcx : schema.dataContext
}
[<Literal>]
let private cUserPath = #"C:\Citect\User"
let private findPath projectName =
Directory.GetDirectories(cUserPath, projectName, SearchOption.AllDirectories)
|> Array.find (fun d -> d.Contains("web") |> not)
let private createPK (cn : IDbConnection) =
let cm = cn.CreateCommand()
cm.CommandText <- "ALTER TABLE Variable ADD PRIMARY KEY (Name)"
try
cn.Open()
cm.ExecuteNonQuery() |> ignore
finally cn.Close()
let internal connect projectName =
let path' = findPath projectName
let connString = sprintf connStringFormat path'
let transOptions = { Timeout = TimeSpan.FromSeconds(3.0); IsolationLevel = Transactions.IsolationLevel.DontCreateTransaction }
let dcx' = schema.GetDataContext(connectionString = connString, transactionOptions = transOptions)
dcx'.CreateConnection() |> createPK
{ name = projectName; path = path'; dcx = dcx' }
let internal updVariable (project : Project) variable =
let dcx = project.dcx
let q = query {
for v in dcx.Dbo.Variable do
where (v.Addr = "%MW217.0")
select v
exactlyOne
}
q.Addr <- "QQQ"
dcx.SubmitUpdates()
let internal prj = connect "NPMUG_SCC35"
updVariable prj ()
I am attempting to port an existing project of mine (a web scraper) from Python to F#, in order to learn F#. A component of the program saves compresses large strings (raw HTML) using LZMA, and stores it in SQLite in a makeshift key value table. The HTML string should always be unicode.
Because I am an F# beginner and this requires a lot of .NET interop, I am very confused as to how to accomplish this.
I would like to know how to do this properly in F#, and using LZMA instead of GZip.
Edit
I had difficulty finding an LZMA2 compatible .NET library, as LZMA-SDK uses LZMA1. This would not have been compatible with my existing data compressed using LZMA2. Therefore, along with help from comments I went ahead and implemented this using Gzip.
This uses Gzip for compression and is compatible with the gzip.compress/gzip.decompress functions in Python 3.5.
#if INTERACTIVE
#r "../packages/System.Data.SQLite.Core/lib/net46/System.Data.SQLite.dll"
#endif
open System.IO
open System.IO.Compression
open System.Data.SQLite
let compressString (s:string) =
let bs = System.Text.Encoding.UTF8.GetBytes(s)
use outStream = new MemoryStream()
use gzOutStream = new GZipStream(outStream, CompressionMode.Compress, false)
gzOutStream.Write(bs, 0, bs.Length)
outStream.ToArray()
let decompressString (bs:byte[]) =
use newInStream = new MemoryStream(bs)
use gzOutStream = new GZipStream(newInStream, CompressionMode.Decompress, false)
use sr = new StreamReader(gzOutStream)
sr.ReadToEnd()
let insert dbc (key:string) (value:string) =
let compressed = compressString value
let cmd = new SQLiteCommand("INSERT into kvt (key, value) VALUES (#key, #value)", dbc)
cmd.Parameters.Add(new SQLiteParameter("#key", key)) |> ignore
cmd.Parameters.Add(new SQLiteParameter("#value", compressed)) |> ignore
let res = cmd.ExecuteNonQuery()
res
let fetch dbc (key:string) =
let cmd = new SQLiteCommand("SELECT value FROM kvt WHERE key = #key", dbc)
cmd.Parameters.Add(new SQLiteParameter("#key", key)) |> ignore
let reader = cmd.ExecuteReader()
reader.Read() |> ignore
let compressed = unbox<byte[]> reader.["value"]
decompressString compressed
let create() =
System.Data.SQLite.SQLiteConnection.CreateFile("mydb.sqlite")
let dbc = new SQLiteConnection("Data Source=mydb.sqlite;Version=3;")
dbc.Open()
let cmd = new SQLiteCommand("CREATE TABLE kvt (key TEXT PRIMARY KEY, value BLOB)", dbc)
let res = cmd.ExecuteNonQuery()
dbc
Im trying to populate list with my own type.
let getUsers =
use connection = openConnection()
let getString = "select * from Accounts"
use sqlCommand = new SqlCommand(getString, connection)
try
let usersList = [||]
use reader = sqlCommand.ExecuteReader()
while reader.Read() do
let floresID = reader.GetString 0
let exName = reader.GetString 1
let exPass = reader.GetString 2
let user = [floresID=floresID; exName=exName; exPass=exPass]
// what here?
()
with
| :? SqlException as e -> printfn "Došlo k chybě úrovni připojení:\n %s" e.Message
| _ -> printfn "Neznámá výjimka."
In C# I would just add new object into userList. How can I add new user into list? Or is it better approach to get some sort of list with data from database?
Easiest way to do this is with a type provider, so you can abstract away the database. You can use SqlDataConnection for SQLServer, SqlProvider for everything (incl. SQLServer), and also SQLClient for SQLServer.
Here is an example with postgres's dvdrental (sample) database for SQLProvider:
#r #"..\packages\SQLProvider.1.0.33\lib\FSharp.Data.SqlProvider.dll"
#r #"..\packages\Npgsql.3.1.8\lib\net451\Npgsql.dll"
open System
open FSharp.Data.Sql
open Npgsql
open NpgsqlTypes
open System.Linq
open System.Xml
open System.IO
open System.Data
let [<Literal>] dbVendor = Common.DatabaseProviderTypes.POSTGRESQL
let [<Literal>] connString1 = #"Server=localhost;Database=dvdrental;User Id=postgres;Password=root"
let [<Literal>] resPath = #"C:\Users\userName\Documents\Visual Studio 2015\Projects\Postgre2\packages\Npgsql.3.1.8\lib\net451"
let [<Literal>] indivAmount = 1000
let [<Literal>] useOptTypes = true
//create the type for the database, based on the connection string, etc. parameters
type sql = SqlDataProvider<dbVendor,connString1,"",resPath,indivAmount,useOptTypes>
//set up the datacontext, ideally you would use `use` here :-)
let ctx = sql.GetDataContext()
let actorTbl = ctx.Public.Actor //alias the table
//set up the type, in this case Records:
type ActorName = {
firstName:string
lastName:string}
//extract the data with a query expression, this gives you type safety and intellisense over SQL (but also see the SqlClient type provider above):
let qry = query {
for row in actorTbl do
select ({firstName=row.FirstName;lastName=row.LastName})
}
//seq is lazy so do all kinds of transformations if necessary then manifest it into a list or array:
qry |> Seq.toArray
The two important parts are defining the Actor record, and then in the query extracting the fields into a sequence of Actor records. You can then manifest into a list or array if necessary.
But you can also stick to your original solution. In that case just wrap the .Read() into a seq:
First define the type:
type User = {
floresID: string
exName: string
exPass: string
}
Then extract the data:
let recs = cmd.ExecuteReader() // execute the SQL Command
//extract the users into a sequence of records:
let users =
seq {
while recs.Read() do
yield {floresID=recs.[0].ToString()
exName=recs.[1].ToString()
exPass=recs.[2].ToString()
}
} |> Seq.toArray
Taking your code, you can use list expression:
let getUsers =
use connection = openConnection()
let getString = "select * from Accounts"
use sqlCommand = new SqlCommand(getString, connection)
try
[
use reader = sqlCommand.ExecuteReader()
while reader.Read() do
let floresID = reader.GetString 0
let exName = reader.GetString 1
let exPass = reader.GetString 2
let user = [floresID=floresID; exName=exName; exPass=exPass]
yield user
]
with
| :? SqlException as e -> failwithf "Došlo k chybě úrovni připojení:\n %s" e.Message
| _ -> failwithf "Neznámá výjimka."
That being said, I'd use FSharp.Data.SqlClient library so all of that boiler plate becomes a single line with added benefit of type safety (if you change the query, the code will have compile time error which are obvious to fix).
I am trying out the samples for FsSql and I seem to be stuck on how to properly use the Sql.execReaderF function. The example code uses an int parameter but I have a string. The following code blocks show my attempts. Does FsSql only support int for this function maybe?
Setup code:
module FsSqlTests
open System
open System.Data
open System.Data.SqlClient
open NUnit.Framework
open Swensen.Unquote
let openConn() =
let conn = new SqlConnection(#"Data Source=MYSERVER;Initial Catalog=MYDB;Integrated Security=True")
conn.Open()
conn :> IDbConnection
let connMgr = Sql.withNewConnection openConn
let P = Sql.Parameter.make
let execReader sql = Sql.execReader connMgr sql
let execReaderf sql = Sql.execReaderF connMgr sql
Using Sql.execReader (Test case passes using this one)
let selectSummaryByeFolderName eFolderName =
execReader "select summary from ework.V_DQ_Iccm_Activity_By_Team WHERE efoldername = #eFolderName"
[P("#eFolderName", eFolderName)]
Using Sql.execReaderF (Test case fails using this one)
let selectSummaryByeFolderName =
execReaderf "select summary from ework.V_DQ_Iccm_Activity_By_Team WHERE efoldername = '%s'"
Calling code in the test case:
[<TestCase>]
let ``Gets CM summary given eFolderName``() =
let c = selectSummaryByeFolderName "CM008671"
let r = c
|> Seq.ofDataReader
|> Seq.map(fun dr ->
let s =
match dr?summary with
| None -> "No Summary"
| Some x -> x
s)
|> Seq.length
test <# r > 0 #>
How can I modify my call to execReaderF to make it pass the parameter and run correctly?
UPDATE:
I tried it out with an integer parameter and it works fine. It seems the function may only support integers.
let selectSummaryByCallPriority =
execReaderf "select top 10 summary from ework.V_DQ_Iccm_Activity_By_Team WHERE callpriority = %d"
I had a look at the implementation to try and verify this but it's over my head. Anyway the Sql.execReader function works fine for other datatypes so I can just switch to that function for my string parameters.
I am back again, this time with a question on writing service in F#. I cannot seem to install the service using installutil. It gives me the following error.
$ installutil atfwindowsservice.exe
Microsoft (R) .NET Framework Installation utility Version 4.0.30319.18408
Copyright (C) Microsoft Corporation. All rights reserved.
Running a transacted installation.
Beginning the Install phase of the installation.
See the contents of the log file for the C:\Dev\ATF\output\bin\Debug\atfwindowsservice.exe assembly's progress.
The file is located at C:\Dev\ATF\output\bin\Debug\atfwindowsservice.InstallLog.
Installing assembly 'C:\Dev\ATF\output\bin\Debug\atfwindowsservice.exe'.
Affected parameters are:
logtoconsole =
logfile = C:\Dev\ATF\output\bin\Debug\atfwindowsservice.InstallLog
assemblypath = C:\Dev\ATF\output\bin\Debug\atfwindowsservice.exe
No public installers with the RunInstallerAttribute.Yes attribute could be found in the C:\Dev\ATF\output\bin\Debug\atfwindowsservice.exe assembly.
The code is given below. Any help is appreciated and thanks in advance.
Ramesh
namespace service
open System.ServiceProcess
open System.Runtime.Remoting
open System.Runtime.Remoting.Channels
type atf() =
inherit ServiceBase(ServiceName = "atf win service")
override x.OnStart(args) = ()
override x.OnStop() = ()
The registering the service code:
// Learn more about F# at http://fsharp.net
// See the 'F# Tutorial' project for more help.=
open System
open System.ComponentModel
open System.Configuration.Install
open System.ServiceProcess
[<RunInstaller(true)>]
type FSharpServiceInstaller() =
inherit Installer()
do
// Specify properties of the hosting process
new ServiceProcessInstaller(Account = ServiceAccount.LocalSystem) |> base.Installers.Add |> ignore
// Specify properties of the service running inside the process
new ServiceInstaller( DisplayName = "F# ATF Service", ServiceName = "atf",StartType = ServiceStartMode.Automatic ) |> base.Installers.Add |> ignore
// Run the chat service when the process starts
module Main =
ServiceBase.Run [| new service.atf() :> ServiceBase |]
I had the same problem. I eventually added the following code which works nicely and has the added benefit of not requiring installutil.exe. The service is able to install/uninstall itself by passing in the correct command line param. Keep all your code and add the following:
module Program =
let getInstaller() =
let installer = new AssemblyInstaller(typedefof<atf>.Assembly, null);
installer.UseNewContext <- true
installer
let installService() =
let installer = getInstaller()
let dic = new System.Collections.Hashtable()
installer.Install(dic)
installer.Commit(dic)
let uninstallService() =
let installer = getInstaller()
let dic = new System.Collections.Hashtable()
installer.Uninstall(dic)
[<EntryPoint>]
let main (args:string[]) =
match (args |> Seq.length) with
|1 -> match (args.[0]) with
|"-install" -> installService()
|"-uninstall" -> uninstallService()
|_-> failwith "Unrecognized param %s" args.[0]
|_ -> ServiceBase.Run [| new atf() :> ServiceBase |]
0
To install you can execute the following from the command line:
atfwindowsservice.exe -install
I figured out how to write a self installing service using other examples on the web, especially this post on stack was useful:
http://pingfu.net/programming/2011/08/11/creating-a-self-installing-windows-service-with-csharp.html
open System
open System.ServiceProcess
open System.Windows
open System.Threading
open System.Windows.Forms
open System.ComponentModel
open System.Configuration.Install
open System.Reflection
open Microsoft.Win32
type ATFServiceInstaller() =
inherit Installer()
let spi_ = new ServiceProcessInstaller(Account = ServiceAccount.LocalSystem)
let si_ = new ServiceInstaller( DisplayName = "ATF Service", Description="ATF service", ServiceName = "atf",StartType = ServiceStartMode.Automatic )
let dic_ = new System.Collections.Hashtable()
let SVC_SERVICE_KET = #"SYSTEM\CurrentControlSet\Services"
member this.install () =
base.Installers.Add(spi_) |> ignore
let ret = base.Installers.Add(si_)
let apath = sprintf "/assemblypath=%s" (Assembly.GetExecutingAssembly().Location)
let ctx = [|apath; "/logToConsole=false"; "/showCallStack"|]
this.Context <- new InstallContext("atfserviceinstall.log", ctx)
base.Install(dic_)
base.Commit(dic_)
member this.uninstall() =
base.Installers.Add(spi_) |> ignore
let ret = base.Installers.Add(si_)
let apath = sprintf "/assemblypath=%s" (Assembly.GetExecutingAssembly().Location)
let ctx = [|apath; "/logToConsole=false"; "/showCallStack"|]
this.Context <- new InstallContext("atfserviceinstall.log", ctx)
base.Uninstall(null)
module Main =
try
let args = Environment.GetCommandLineArgs()
match (args |> Seq.length) with
| 2 -> match (args.[1]) with
| "-install" -> let installer = new ATFServiceInstaller()
installer.install()
installer.Dispose()
| "-uninstall" -> let installer = new ATFServiceInstaller()
installer.uninstall()
installer.Dispose()
| _ -> failwith "Unrecognized param %s" args.[0]
| _ -> ServiceBase.Run [| new atfservice.ATFService() :> ServiceBase |]
with
| _ as ex -> MessageBox.Show(ex.ToString()) |> ignore
I came across this question while having the same issue. I still needed to use InstallUtil.exe in the deployment and find out that the problem with your original code was a missing namespace in the main file.
I have found this framework for hosting .NET services http://topshelf-project.com/ which makes the development much easier and basically lets you create a console application which you can debug and also has a built-in Windows/Mono service installer. The only downside for me was a missing support for installation with InstallUtil.exe again but there is a solution for that too. Instead of adding ServiceProcessInstaller and ServiceInstaller to Installers in the class inherited from Installer override Install and Uninstall methods and make them run your executable with install/unistall parameter.
[<RunInstaller(true)>]
type public FSharpServiceInstaller() =
inherit Installer()
override __.Install(stateSaver : System.Collections.IDictionary) =
let assemblyPath = __.Context.Parameters.["assemblypath"]
stateSaver.Add(assemblyIdentifier, assemblyPath)
// runProcess assemblyPath "install"
base.Install(stateSaver)
override __.Uninstall(savedState : System.Collections.IDictionary) =
let assemblyPath = savedState.[assemblyIdentifier].ToString()
// runProcess assemblyPath "uninstall"
base.Uninstall(savedState)
Full code at: https://gist.github.com/jbezak/eda4cc5864059b717e71beaec47db2d9