text:
All notable changes to this project will be documented in this file.
Each new release typically also includes the latest modulesync defaults.
These should not affect the functionality of the module.

## [v7.2.0](https://github.com/voxpupuli/puppet-splunk/tree/v7.2.0) (2018-06-14)

[Full Changelog](https://github.com/voxpupuli/puppet-splunk/compare/v7.1.0...v7.2.0)

**Implemented enhancements:**

- New Custom Type For Managing .meta files [\#176](https://github.com/voxpupuli/puppet-splunk/pull/176) ([michaelweiser](https://github.com/michaelweiser))

**Fixed bugs:**

- fix unknown variable: staged\_package [\#179](https://github.com/voxpupuli/puppet-splunk/pull/179) ([bastelfreak](https://github.com/bastelfreak))

**Closed issues:**

- Bump dependency constraint for the puppetlabs/inifile module  [\#172](https://github.com/voxpupuli/puppet-splunk/issues/172)

**Merged pull requests:**

- Allow this module to be installed with puppet-archive 3.0.0 [\#184](https://github.com/voxpupuli/puppet-splunk/pull/184) ([mpdude](https://github.com/mpdude))
- Allow forwarder to fully support alternate install directories [\#183](https://github.com/voxpupuli/puppet-splunk/pull/183) ([Nekototori](https://github.com/Nekototori))
- drop EOL OSs; fix puppet version range [\#182](https://github.com/voxpupuli/puppet-splunk/pull/182) ([bastelfreak](https://github.com/bastelfreak))
- Rely on beaker-hostgenerator for docker nodesets [\#181](https://github.com/voxpupuli/puppet-splunk/pull/181) ([ekohl](https://github.com/ekohl))
- bump puppet to latest supported version 4.10.0 [\#178](https://github.com/voxpupuli/puppet-splunk/pull/178) ([bastelfreak](https://github.com/bastelfreak))
- Remove unsupported use of proc in title patterns [\#174](https://github.com/voxpupuli/puppet-splunk/pull/174) ([treydock](https://github.com/treydock))
- \(maint\) Bumping inifile dependency from \> 2.0.0 to \> 3.0.0 [\#173](https://github.com/voxpupuli/puppet-splunk/pull/173) ([TraGicCode](https://github.com/TraGicCode))

## [v7.1.0](https://github.com/voxpupuli/puppet-splunk/tree/v7.1.0) (2017-12-09)

[Full Changelog](https://github.com/voxpupuli/puppet-splunk/compare/v7.0.0...v7.1.0)

**Implemented enhancements:**

- Update splunk installer directory structure + enable automated tests [\#149](https://github.com/voxpupuli/puppet-splunk/issues/149)

**Fixed bugs:**

- Error on using ini\_setting [\#77](https://github.com/voxpupuli/puppet-splunk/issues/77)
- The splunk::forwarder::purge\_inputs generates error  [\#69](https://github.com/voxpupuli/puppet-splunk/issues/69)

**Closed issues:**

- Agent run fails: no parameter named 'purge\_forwarder\_deploymentclient'  [\#158](https://github.com/voxpupuli/puppet-splunk/issues/158)
- Allow /opt/splunkforwarder/var/run/splunk splunkd.pid to run as splunk [\#154](https://github.com/voxpupuli/puppet-splunk/issues/154)
- Erro deploying Splunk both Server and Forwarder [\#76](https://github.com/voxpupuli/puppet-splunk/issues/76)
- Need a `splunkforwarder\_input` defined resource type [\#75](https://github.com/voxpupuli/puppet-splunk/issues/75)

**Merged pull requests:**

- release 7.1.0 [\#165](https://github.com/voxpupuli/puppet-splunk/pull/165) ([bastelfreak](https://github.com/bastelfreak))
- Update README [\#162](https://github.com/voxpupuli/puppet-splunk/pull/162) ([arjenz](https://github.com/arjenz))
- replace validate\_string with assert\_type [\#161](https://github.com/voxpupuli/puppet-splunk/pull/161) ([bastelfreak](https://github.com/bastelfreak))

## [v7.0.0](https://github.com/voxpupuli/puppet-splunk/tree/v7.0.0) (2017-10-04)

[Full Changelog](https://github.com/voxpupuli/puppet-splunk/compare/v6.3.1...v7.0.0)

**Breaking changes:**

- BREAKING: Ability to download from download.splunk.com [\#150](https://github.com/voxpupuli/puppet-splunk/pull/150) ([TraGicCode](https://github.com/TraGicCode))

**Implemented enhancements:**

- Add serverclass type [\#147](https://github.com/voxpupuli/puppet-splunk/pull/147) ([TraGicCode](https://github.com/TraGicCode))

**Closed issues:**

- Create a type to manage deployment server "ServerClasses" [\#146](https://github.com/voxpupuli/puppet-splunk/issues/146)

**Merged pull requests:**

- v7.0.0 Release [\#152](https://github.com/voxpupuli/puppet-splunk/pull/152) ([TraGicCode](https://github.com/TraGicCode))

## [v6.3.1](https://github.com/voxpupuli/puppet-splunk/tree/v6.3.1) (2017-09-26)

[Full Changelog](https://github.com/voxpupuli/puppet-splunk/compare/v6.3.0...v6.3.1)

**Fixed bugs:**

- Fix config file location for splunkforwarder\_deploymentclient, deployment client, and alert\_actions. [\#144](https://github.com/voxpupuli/puppet-splunk/pull/144) ([treydock](https://github.com/treydock))

**Merged pull requests:**

- Release v6.3.1 [\#145](https://github.com/voxpupuli/puppet-splunk/pull/145) ([TraGicCode](https://github.com/TraGicCode))

## [v6.3.0](https://github.com/voxpupuli/puppet-splunk/tree/v6.3.0) (2017-09-25)

[Full Changelog](https://github.com/voxpupuli/puppet-splunk/compare/v6.2.0...v6.3.0)

**Implemented enhancements:**

- Add splunk\_alert\_actions type. [\#142](https://github.com/voxpupuli/puppet-splunk/pull/142) ([TraGicCode](https://github.com/TraGicCode))
- Add deploymentclient types [\#141](https://github.com/voxpupuli/puppet-splunk/pull/141) ([TraGicCode](https://github.com/TraGicCode))
- Add Purge + File Management for splunk\_uiprefs [\#124](https://github.com/voxpupuli/puppet-splunk/pull/124) ([treydock](https://github.com/treydock))

**Closed issues:**

- Add Windows Server 2016 + Ubuntu 16.04 to Supported OS's [\#137](https://github.com/voxpupuli/puppet-splunk/issues/137)

**Merged pull requests:**

- v6.3.0 release [\#143](https://github.com/voxpupuli/puppet-splunk/pull/143) ([TraGicCode](https://github.com/TraGicCode))

## [v6.2.0](https://github.com/voxpupuli/puppet-splunk/tree/v6.2.0) (2017-09-24)

[Full Changelog](https://github.com/voxpupuli/puppet-splunk/compare/v6.1.0...v6.2.0)

**Implemented enhancements:**

- Replace Staging with archive module [\#127](https://github.com/voxpupuli/puppet-splunk/issues/127)

**Closed issues:**

- Evaluation Error: Unknown variable: 'splunk::params::forwarder\_install\_options' [\#97](https://github.com/voxpupuli/puppet-splunk/issues/97)

**Merged pull requests:**

- 6.2.0 Release [\#139](https://github.com/voxpupuli/puppet-splunk/pull/139) ([TraGicCode](https://github.com/TraGicCode))
- Added ubuntu 16.04 & 2016 + 2008R2 support. [\#138](https://github.com/voxpupuli/puppet-splunk/pull/138) ([TraGicCode](https://github.com/TraGicCode))
- Replace staging module with archive module [\#128](https://github.com/voxpupuli/puppet-splunk/pull/128) ([TraGicCode](https://github.com/TraGicCode))

## [v6.1.0](https://github.com/voxpupuli/puppet-splunk/tree/v6.1.0) (2017-09-20)

[Full Changelog](https://github.com/voxpupuli/puppet-splunk/compare/v6.0.0...v6.1.0)

**Implemented enhancements:**

- Ability to manage ui-prefs.conf [\#103](https://github.com/voxpupuli/puppet-splunk/issues/103)
- Added splunkforwarder\_server and splunkforwarder\_limits providers/types. [\#118](https://github.com/voxpupuli/puppet-splunk/pull/118) ([nicholaspearson](https://github.com/nicholaspearson))
- Allow redirecting of settings into custom contexts [\#87](https://github.com/voxpupuli/puppet-splunk/pull/87) ([michaelweiser](https://github.com/michaelweiser))

**Fixed bugs:**

- splunkd\_port not defined for forwarder [\#133](https://github.com/voxpupuli/puppet-splunk/issues/133)
- concat dependency is missing from metadata.json [\#122](https://github.com/voxpupuli/puppet-splunk/issues/122)
- splunkforwarder\_server not working [\#121](https://github.com/voxpupuli/puppet-splunk/issues/121)
- Fix .conf file modes for Windows forwarders [\#114](https://github.com/voxpupuli/puppet-splunk/pull/114) ([natemccurdy](https://github.com/natemccurdy))
- Allow for installing packages from UNC paths on Windows [\#113](https://github.com/voxpupuli/puppet-splunk/pull/113) ([natemccurdy](https://github.com/natemccurdy))
- Fix resource ordering in splunk class so that splunk types will properly notify services [\#91](https://github.com/voxpupuli/puppet-splunk/pull/91) ([treydock](https://github.com/treydock))

**Closed issues:**

- Ruby load error when using splunk::forwarder on Windows [\#110](https://github.com/voxpupuli/puppet-splunk/issues/110)

**Merged pull requests:**

- Release v6.1.0 [\#135](https://github.com/voxpupuli/puppet-splunk/pull/135) ([TraGicCode](https://github.com/TraGicCode))
- SplunkForwarder has no concept of a splunkd\_port [\#134](https://github.com/voxpupuli/puppet-splunk/pull/134) ([TraGicCode](https://github.com/TraGicCode))
- Remove unused spec\_helper\_system.rb [\#132](https://github.com/voxpupuli/puppet-splunk/pull/132) ([wyardley](https://github.com/wyardley))
- SplunkForwarder has no concept of a server\_service.  [\#130](https://github.com/voxpupuli/puppet-splunk/pull/130) ([TraGicCode](https://github.com/TraGicCode))
- Update missing concat dependency in acceptance tests. [\#129](https://github.com/voxpupuli/puppet-splunk/pull/129) ([TraGicCode](https://github.com/TraGicCode))
- wire up splunkforwarder\_server type [\#126](https://github.com/voxpupuli/puppet-splunk/pull/126) ([alexcreek](https://github.com/alexcreek))
- Added missing concat dependency to metadata.json + .fixtures.yml [\#123](https://github.com/voxpupuli/puppet-splunk/pull/123) ([TraGicCode](https://github.com/TraGicCode))
- Fix typo in extension filename [\#117](https://github.com/voxpupuli/puppet-splunk/pull/117) ([gregoirefra](https://github.com/gregoirefra))
- This commit adds the pkg\_provider 'chocolatey' to the module [\#108](https://github.com/voxpupuli/puppet-splunk/pull/108) ([ralfbosz](https://github.com/ralfbosz))
- Ability to configure ui-prefs.conf [\#104](https://github.com/voxpupuli/puppet-splunk/pull/104) ([TraGicCode](https://github.com/TraGicCode))
- Set `forwarder\_install\_options` to be `undef` for OS other than Windows. [\#99](https://github.com/voxpupuli/puppet-splunk/pull/99) ([shadow999](https://github.com/shadow999))

## [v6.0.0](https://github.com/voxpupuli/puppet-splunk/tree/v6.0.0) (2017-05-25)

[Full Changelog](https://github.com/voxpupuli/puppet-splunk/compare/v5.1.0...v6.0.0)

**Fixed bugs:**

- Fix Windows Forwarder Installdir [\#102](https://github.com/voxpupuli/puppet-splunk/pull/102) ([TraGicCode](https://github.com/TraGicCode))

**Closed issues:**

- Support .conf files in non-standard locations [\#88](https://github.com/voxpupuli/puppet-splunk/issues/88)
- Warnings produced when splunk resources are in catalog [\#86](https://github.com/voxpupuli/puppet-splunk/issues/86)

**Merged pull requests:**

- Bump release for 6.0.0 [\#112](https://github.com/voxpupuli/puppet-splunk/pull/112) ([petems](https://github.com/petems))
- Add LICENSE file [\#109](https://github.com/voxpupuli/puppet-splunk/pull/109) ([alexjfisher](https://github.com/alexjfisher))
- This commit sets the splunk\_user for Windows to 'administrator' [\#107](https://github.com/voxpupuli/puppet-splunk/pull/107) ([ralfbosz](https://github.com/ralfbosz))
- Lint fixes [\#101](https://github.com/voxpupuli/puppet-splunk/pull/101) ([treydock](https://github.com/treydock))
- types: Fix purging when section contains '//' [\#96](https://github.com/voxpupuli/puppet-splunk/pull/96) ([iamjamestl](https://github.com/iamjamestl))
- splunk\_config: Only load other splunk types [\#95](https://github.com/voxpupuli/puppet-splunk/pull/95) ([iamjamestl](https://github.com/iamjamestl))
- Remove unneded blank lines [\#93](https://github.com/voxpupuli/puppet-splunk/pull/93) ([roidelapluie](https://github.com/roidelapluie))
- Allow splunk server services to be overridden [\#90](https://github.com/voxpupuli/puppet-splunk/pull/90) ([treydock](https://github.com/treydock))
- Ensure /etc/init.d/splunk is created before splunk services [\#89](https://github.com/voxpupuli/puppet-splunk/pull/89) ([treydock](https://github.com/treydock))
- Update README.md from puppetlabs-splunk [\#85](https://github.com/voxpupuli/puppet-splunk/pull/85) ([Cinderhaze](https://github.com/Cinderhaze))
- Modulesync 0.18.0 [\#83](https://github.com/voxpupuli/puppet-splunk/pull/83) ([bastelfreak](https://github.com/bastelfreak))

## [v5.1.0](https://github.com/voxpupuli/puppet-splunk/tree/v5.1.0) (2017-01-12)

[Full Changelog](https://github.com/voxpupuli/puppet-splunk/compare/v5.0.2...v5.1.0)

**Closed issues:**

- module doesn't notify splunk restart when changing an input [\#63](https://github.com/voxpupuli/puppet-splunk/issues/63)

**Merged pull requests:**

- release 5.1.0 [\#82](https://github.com/voxpupuli/puppet-splunk/pull/82) ([bastelfreak](https://github.com/bastelfreak))
- modulesync 0.16.7 [\#81](https://github.com/voxpupuli/puppet-splunk/pull/81) ([bastelfreak](https://github.com/bastelfreak))
- Bump min version\_requirement for Puppet + deps [\#80](https://github.com/voxpupuli/puppet-splunk/pull/80) ([juniorsysadmin](https://github.com/juniorsysadmin))
- modulesync 0.16.6 [\#79](https://github.com/voxpupuli/puppet-splunk/pull/79) ([bastelfreak](https://github.com/bastelfreak))
- modulesync 0.16.4 [\#78](https://github.com/voxpupuli/puppet-splunk/pull/78) ([bastelfreak](https://github.com/bastelfreak))
- modulesync 0.16.3 [\#74](https://github.com/voxpupuli/puppet-splunk/pull/74) ([bastelfreak](https://github.com/bastelfreak))
- Make the file reference use the variable and not a fixed path [\#73](https://github.com/voxpupuli/puppet-splunk/pull/73) ([ralfbosz](https://github.com/ralfbosz))
- modulesync 0.15.0 [\#72](https://github.com/voxpupuli/puppet-splunk/pull/72) ([bastelfreak](https://github.com/bastelfreak))
- Revert "Release deprecated version" [\#70](https://github.com/voxpupuli/puppet-splunk/pull/70) ([hunner](https://github.com/hunner))
- Add missing badges [\#66](https://github.com/voxpupuli/puppet-splunk/pull/66) ([dhoppe](https://github.com/dhoppe))
- Update based on voxpupuli/modulesync\_config 0.14.1. This might fix \#62 [\#64](https://github.com/voxpupuli/puppet-splunk/pull/64) ([dhoppe](https://github.com/dhoppe))

## [v5.0.2](https://github.com/voxpupuli/puppet-splunk/tree/v5.0.2) (2016-10-05)

[Full Changelog](https://github.com/voxpupuli/puppet-splunk/compare/v5.0.1...v5.0.2)

**Fixed bugs:**

- added package dependency to splunk::addon types [\#59](https://github.com/voxpupuli/puppet-splunk/pull/59) ([crayfishx](https://github.com/crayfishx))

**Closed issues:**

- Invalid parameter key\_val\_separator [\#4](https://github.com/voxpupuli/puppet-splunk/issues/4)
- inheritence and version overrides [\#3](https://github.com/voxpupuli/puppet-splunk/issues/3)
- Add ability to upgrade universal forwarders and servers [\#1](https://github.com/voxpupuli/puppet-splunk/issues/1)

**Merged pull requests:**

- prepped 5.0.2 [\#60](https://github.com/voxpupuli/puppet-splunk/pull/60) ([crayfishx](https://github.com/crayfishx))

## [v5.0.1](https://github.com/voxpupuli/puppet-splunk/tree/v5.0.1) (2016-10-04)

[Full Changelog](https://github.com/voxpupuli/puppet-splunk/compare/v5.0.0...v5.0.1)

**Fixed bugs:**

- Notify service from addons [\#57](https://github.com/voxpupuli/puppet-splunk/pull/57) ([crayfishx](https://github.com/crayfishx))

**Merged pull requests:**

- Prepped 5.0.1 [\#58](https://github.com/voxpupuli/puppet-splunk/pull/58) ([crayfishx](https://github.com/crayfishx))

## [v5.0.0](https://github.com/voxpupuli/puppet-splunk/tree/v5.0.0) (2016-10-03)

[Full Changelog](https://github.com/voxpupuli/puppet-splunk/compare/4.0.0...v5.0.0)

**Closed issues:**

- Add other supported OSes to metadata.json [\#54](https://github.com/voxpupuli/puppet-splunk/issues/54)

**Merged pull requests:**

- prepped 5.0.0 [\#56](https://github.com/voxpupuli/puppet-splunk/pull/56) ([crayfishx](https://github.com/crayfishx))
- Added rspec tests for types and splunk\_config and other bugfixes [\#55](https://github.com/voxpupuli/puppet-splunk/pull/55) ([crayfishx](https://github.com/crayfishx))
- Bug fixes, lint, rubocop, spec tests etc. [\#53](https://github.com/voxpupuli/puppet-splunk/pull/53) ([alexjfisher](https://github.com/alexjfisher))
- Add 'Build Status' badge [\#52](https://github.com/voxpupuli/puppet-splunk/pull/52) ([alexjfisher](https://github.com/alexjfisher))
- Modulesync 0.12.7 [\#51](https://github.com/voxpupuli/puppet-splunk/pull/51) ([bastelfreak](https://github.com/bastelfreak))
- Fixed service restarts with forwarders [\#50](https://github.com/voxpupuli/puppet-splunk/pull/50) ([crayfishx](https://github.com/crayfishx))
- \[REVIEW\] \[MODULES-3520\] refactored types and providers and added purging [\#49](https://github.com/voxpupuli/puppet-splunk/pull/49) ([crayfishx](https://github.com/crayfishx))
- \(maint\) Fix resource order for installing splunk forwarders [\#47](https://github.com/voxpupuli/puppet-splunk/pull/47) ([briancain](https://github.com/briancain))

## [4.0.0](https://github.com/voxpupuli/puppet-splunk/tree/4.0.0) (2016-04-11)

[Full Changelog](https://github.com/voxpupuli/puppet-splunk/compare/3.2.1...4.0.0)

**Merged pull requests:**

- \(maint\) Release prep. [\#45](https://github.com/voxpupuli/puppet-splunk/pull/45) ([bmjen](https://github.com/bmjen))
- Fix typo in README. [\#44](https://github.com/voxpupuli/puppet-splunk/pull/44) ([Ziaunys](https://github.com/Ziaunys))
- Update README with type documentation. [\#43](https://github.com/voxpupuli/puppet-splunk/pull/43) ([Ziaunys](https://github.com/Ziaunys))
- Fix module errors discovered while testing. [\#42](https://github.com/voxpupuli/puppet-splunk/pull/42) ([Ziaunys](https://github.com/Ziaunys))
- Add acceptance test skeleton. [\#41](https://github.com/voxpupuli/puppet-splunk/pull/41) ([Ziaunys](https://github.com/Ziaunys))
- Change file mode to string. [\#40](https://github.com/voxpupuli/puppet-splunk/pull/40) ([Ziaunys](https://github.com/Ziaunys))
- Add a more modern version of our README template with updates. [\#39](https://github.com/voxpupuli/puppet-splunk/pull/39) ([Ziaunys](https://github.com/Ziaunys))
- Various updates and fixes [\#38](https://github.com/voxpupuli/puppet-splunk/pull/38) ([Ziaunys](https://github.com/Ziaunys))
- Update the resource ordering for the new Splunk types. [\#37](https://github.com/voxpupuli/puppet-splunk/pull/37) ([Ziaunys](https://github.com/Ziaunys))
- Add conditional logic for using the staging module [\#36](https://github.com/voxpupuli/puppet-splunk/pull/36) ([Ziaunys](https://github.com/Ziaunys))
- Create additional Splunk types/providers [\#35](https://github.com/voxpupuli/puppet-splunk/pull/35) ([Ziaunys](https://github.com/Ziaunys))
- Add splunk\_server type and provider [\#34](https://github.com/voxpupuli/puppet-splunk/pull/34) ([Ziaunys](https://github.com/Ziaunys))
- Correct the descriptions of existing splunk types [\#33](https://github.com/voxpupuli/puppet-splunk/pull/33) ([Ziaunys](https://github.com/Ziaunys))
- Actually use the variable somewhere [\#32](https://github.com/voxpupuli/puppet-splunk/pull/32) ([kyledecot](https://github.com/kyledecot))
- Server ensure [\#31](https://github.com/voxpupuli/puppet-splunk/pull/31) ([kyledecot](https://github.com/kyledecot))
- Adds param for ensuring forwarder can be the latest argument [\#30](https://github.com/voxpupuli/puppet-splunk/pull/30) ([bmjen](https://github.com/bmjen))
- forwarder::pkg\_provider should default to $splunk::params::pkg\_provider [\#29](https://github.com/voxpupuli/puppet-splunk/pull/29) ([hunner](https://github.com/hunner))
- Fix dependencies [\#28](https://github.com/voxpupuli/puppet-splunk/pull/28) ([mpdude](https://github.com/mpdude))
- \(MODULES-2953\) fix order of assignments [\#27](https://github.com/voxpupuli/puppet-splunk/pull/27) ([DavidS](https://github.com/DavidS))
- added technology addon feature [\#26](https://github.com/voxpupuli/puppet-splunk/pull/26) ([crayfishx](https://github.com/crayfishx))
- config items are collected with tag - make sure it exists [\#25](https://github.com/voxpupuli/puppet-splunk/pull/25) ([asquelt](https://github.com/asquelt))
- Expose splunk forwarder windows install options [\#14](https://github.com/voxpupuli/puppet-splunk/pull/14) ([nanliu](https://github.com/nanliu))

## [3.2.1](https://github.com/voxpupuli/puppet-splunk/tree/3.2.1) (2015-09-08)

[Full Changelog](https://github.com/voxpupuli/puppet-splunk/compare/3.2.0...3.2.1)

**Merged pull requests:**

- 3.2.1 prep [\#24](https://github.com/voxpupuli/puppet-splunk/pull/24) ([underscorgan](https://github.com/underscorgan))
- MODULES-2448 - Improved collector compatibility [\#23](https://github.com/voxpupuli/puppet-splunk/pull/23) ([underscorgan](https://github.com/underscorgan))

## [3.2.0](https://github.com/voxpupuli/puppet-splunk/tree/3.2.0) (2015-07-21)

[Full Changelog](https://github.com/voxpupuli/puppet-splunk/compare/3.1.1...3.2.0)

**Merged pull requests:**

- Release 3.2.0 [\#19](https://github.com/voxpupuli/puppet-splunk/pull/19) ([hunner](https://github.com/hunner))
- Update splunkforwarder\_output [\#18](https://github.com/voxpupuli/puppet-splunk/pull/18) ([hunner](https://github.com/hunner))
- Repository and password changes [\#17](https://github.com/voxpupuli/puppet-splunk/pull/17) ([ckyriakidou](https://github.com/ckyriakidou))
- \(MODULES-2096\) Move default cases last [\#16](https://github.com/voxpupuli/puppet-splunk/pull/16) ([DavidS](https://github.com/DavidS))

## [3.1.1](https://github.com/voxpupuli/puppet-splunk/tree/3.1.1) (2014-12-18)

[Full Changelog](https://github.com/voxpupuli/puppet-splunk/compare/3.1.0...3.1.1)

**Merged pull requests:**

- 3.1.1 prep [\#13](https://github.com/voxpupuli/puppet-splunk/pull/13) ([underscorgan](https://github.com/underscorgan))
- Fix lint issues [\#12](https://github.com/voxpupuli/puppet-splunk/pull/12) ([underscorgan](https://github.com/underscorgan))
- Add solaris sparc parameters. [\#10](https://github.com/voxpupuli/puppet-splunk/pull/10) ([nanliu](https://github.com/nanliu))

## [3.1.0](https://github.com/voxpupuli/puppet-splunk/tree/3.1.0) (2014-08-06)

[Full Changelog](https://github.com/voxpupuli/puppet-splunk/compare/3.0.1...3.1.0)

**Merged pull requests:**

- 3.1.0 prep [\#11](https://github.com/voxpupuli/puppet-splunk/pull/11) ([underscorgan](https://github.com/underscorgan))
- Update the name of the module for PMT install. [\#8](https://github.com/voxpupuli/puppet-splunk/pull/8) ([metcalfc](https://github.com/metcalfc))
- Add integration tests and fix forwarder bug [\#7](https://github.com/voxpupuli/puppet-splunk/pull/7) ([hunner](https://github.com/hunner))
- Add T&Ps for Splunk indexer and forwarder props and transforms [\#6](https://github.com/voxpupuli/puppet-splunk/pull/6) ([hunner](https://github.com/hunner))
- Add dependency on stdlib \>= 2.4.0 [\#5](https://github.com/voxpupuli/puppet-splunk/pull/5) ([hunner](https://github.com/hunner))

## [3.0.1](https://github.com/voxpupuli/puppet-splunk/tree/3.0.1) (2013-12-02)

[Full Changelog](https://github.com/voxpupuli/puppet-splunk/compare/3.0.0...3.0.1)

## [3.0.0](https://github.com/voxpupuli/puppet-splunk/tree/3.0.0) (2013-11-22)

[Full Changelog](https://github.com/voxpupuli/puppet-splunk/compare/2.0.1...3.0.0)

## [2.0.1](https://github.com/voxpupuli/puppet-splunk/tree/2.0.1) (2013-10-25)

[Full Changelog](https://github.com/voxpupuli/puppet-splunk/compare/2.0.0...2.0.1)

## [2.0.0](https://github.com/voxpupuli/puppet-splunk/tree/2.0.0) (2013-09-30)

[Full Changelog](https://github.com/voxpupuli/puppet-splunk/compare/0.3.0...2.0.0)

## [0.3.0](https://github.com/voxpupuli/puppet-splunk/tree/0.3.0) (2013-08-27)

[Full Changelog](https://github.com/voxpupuli/puppet-splunk/compare/0.2.0...0.3.0)

**Merged pull requests:**

- typo in puppet module install command [\#2](https://github.com/voxpupuli/puppet-splunk/pull/2) ([fiddyspence](https://github.com/fiddyspence))

## [0.2.0](https://github.com/voxpupuli/puppet-splunk/tree/0.2.0) (2012-12-18)

[Full Changelog](https://github.com/voxpupuli/puppet-splunk/compare/b61fce61714ec54dafd9364a4f102647cd5cf185...0.2.0)

\* *This Changelog was automatically generated by [github_changelog_generator](https://github.com/skywinder/Github-Changelog-Generator)*


text:
using System;
using System.Diagnostics;
using System.IO;
using NUnit.Framework;
using System.Reflection;
using Yargon.Parser.Sdf.ParseTrees;
using Yargon.Parser.Sdf.Productions.IO;
using Yargon.Parsing;
using Yargon.ATerms;
using Yargon.ATerms.IO;

namespace Yargon.Parser.Sdf
{
	[TestFixture]
	public class Example2
	{
	    private const string Name = "Example2";

        [Test]
		public void ReadParseTableFromFile()
		{
            // Arrange
		    var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream($"Yargon.Parser.Sdf.{Name}.tbl");
            Debug.Assert(stream != null);
            var termFactory = new TrivialTermFactory();
            var productionFormat = new TermProductionFormat(termFactory);
            var reader = new SdfParseTableReader(productionFormat);

            // Act
            var parseTable = reader.Read(new StreamReader(stream));

            // Assert
            // No exceptions.

            // Cleanup
            stream.Dispose();
		}

        [Test]
	    public void ParseString()
        {
            // Arrange
            var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream($"Yargon.Parser.Sdf.{Name}.tbl");
            var termFactory = new TrivialTermFactory();
            var productionFormat = new TermProductionFormat(termFactory);
            var reader = new SdfParseTableReader(productionFormat);
            var parseTable = reader.Read(new StreamReader(stream));
            var parseTreeBuilder = new TrivialParseNodeFactory();
            var errorHandler = new FailingErrorHandler<SdfStateRef, Token<CodePoint>>();
            var parser = new YargonParser<SdfStateRef, Token<CodePoint>, IParseNode>(parseTable, parseTreeBuilder, errorHandler);

            // Act
            string input = "keyword";
            var result = parser.Parse(new CharacterProvider(new StringReader(input)));

            // Assert
            Assert.That(result.Success, Is.True);
            Assert.That(result.Tree.ToString(), Is.EqualTo("appl(\"<START>\" -> CF-layout?CF-\"Start\"CF-layout?; [appl(CF-layout? -> ; []), appl(CF-\"Start\" -> \"\"keyword\"\"; [appl(\"\"keyword\"\" -> [107][101][121][119][111][114][100]; [107, 101, 121, 119, 111, 114, 100])]), appl(CF-layout? -> ; [])])"));

            // Cleanup
            stream.Dispose();
        }
    }
}

redpajama_set_name: RedPajamaGithub


text:
'use strict';

/**
* Create an iterator which replicates each iterated value a specified number of times.
*
* @module @stdlib/iter/replicate
*
* @example
* var array2iterator = require( '@stdlib/array/to-iterator' );
* var iterReplicate = require( '@stdlib/iter/replicate' );
*
* var src = array2iterator( [ 1, 2, 3, 4 ] );
* var iter = iterReplicate( src, 2 );
*
* var v = iter.next().value;
* // returns 1
*
* v = iter.next().value;
* // returns 1
*
* v = iter.next().value;
* // returns 2
*
* v = iter.next().value;
* // returns 2
*
* // ...
*/

// MODULES //

var main = require( './main.js' );

// EXPORTS //

module.exports = main;


text:
package org.kuali.kra.award.home;

import org.kuali.coeus.sys.framework.model.KcPersistableBusinessObjectBase;

/**
 *
 * This class represents AwardStatus BO
 */
public class AwardStatus extends KcPersistableBusinessObjectBase {

    private static final long serialVersionUID = 7554440681643595144L;

    private String statusCode;

    private String description;

    public AwardStatus() {
    }

    public String getStatusCode() {
        return statusCode;
    }

    public void setStatusCode(String statusCode) {
        this.statusCode = statusCode;
    }

    public String getDescription() {
        return description;
    }

    public void setDescription(String description) {
        this.description = description;
    }

    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + ((description == null) ? 0 : description.hashCode());
        result = prime * result + ((statusCode == null) ? 0 : statusCode.hashCode());
        return result;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj == null) {
            return false;
        }
        if (getClass() != obj.getClass()) return false;
        AwardStatus other = (AwardStatus) obj;
        if (description == null) {
            if (other.description != null) {
                return false;
            }
        } else if (!description.equals(other.description)) {
            return false;
        }
        if (statusCode == null) {
            if (other.statusCode != null) {
                return false;
            }
        } else if (!statusCode.equals(other.statusCode)) {
            return false;
        }
        return true;
    }
}


text:
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using Moq;
using NUnit.Framework;
using Umbraco.Core;
using Umbraco.Core.Composing;
using Umbraco.Core.IO;
using Umbraco.Core.Logging;
using Umbraco.Core.Scoping;
using Umbraco.Tests.TestHelpers;

namespace Umbraco.Tests.IO
{
    [TestFixture]
    public class ShadowFileSystemTests
    {
        // tested:
        // only 1 instance of this class is created
        // SetUp and TearDown run before/after each test
        // SetUp does not start before the previous TearDown returns

        [SetUp]
        public void SetUp()
        {
            SafeCallContext.Clear();
            ClearFiles();
            FileSystems.ResetShadowId();
        }

        [TearDown]
        public void TearDown()
        {
            SafeCallContext.Clear();
            ClearFiles();
            FileSystems.ResetShadowId();
        }

        private static void ClearFiles()
        {
            TestHelper.DeleteDirectory(IOHelper.MapPath("FileSysTests"));
            TestHelper.DeleteDirectory(IOHelper.MapPath(SystemDirectories.TempData.EnsureEndsWith('/') + "ShadowFs"));
        }

        private static string NormPath(string path)
        {
            return path.ToLowerInvariant().Replace("\\", "/");
        }

        [Test]
        public void ShadowDeleteDirectory()
        {
            var path = IOHelper.MapPath("FileSysTests");
            Directory.CreateDirectory(path);
            Directory.CreateDirectory(path + "/ShadowTests");
            Directory.CreateDirectory(path + "/ShadowSystem");

            var fs = new PhysicalFileSystem(path + "/ShadowTests/", "ignore");
            var sfs = new PhysicalFileSystem(path + "/ShadowSystem/", "ignore");
            var ss = new ShadowFileSystem(fs, sfs);

            Directory.CreateDirectory(path + "/ShadowTests/d1");
            Directory.CreateDirectory(path + "/ShadowTests/d2");

            var files = fs.GetFiles("");
            Assert.AreEqual(0, files.Count());

            var dirs = fs.GetDirectories("");
            Assert.AreEqual(2, dirs.Count());
            Assert.IsTrue(dirs.Contains("d1"));
            Assert.IsTrue(dirs.Contains("d2"));

            ss.DeleteDirectory("d1");

            Assert.IsTrue(Directory.Exists(path + "/ShadowTests/d1"));
            Assert.IsTrue(fs.DirectoryExists("d1"));
            Assert.IsFalse(ss.DirectoryExists("d1"));

            dirs = ss.GetDirectories("");
            Assert.AreEqual(1, dirs.Count());
            Assert.IsTrue(dirs.Contains("d2"));
        }

        [Test]
        public void ShadowDeleteDirectoryInDir()
        {
            var path = IOHelper.MapPath("FileSysTests");
            Directory.CreateDirectory(path);
            Directory.CreateDirectory(path + "/ShadowTests");
            Directory.CreateDirectory(path + "/ShadowSystem");

            var fs = new PhysicalFileSystem(path + "/ShadowTests/", "ignore");
            var sfs = new PhysicalFileSystem(path + "/ShadowSystem/", "ignore");
            var ss = new ShadowFileSystem(fs, sfs);

            Directory.CreateDirectory(path + "/ShadowTests/sub");
            Directory.CreateDirectory(path + "/ShadowTests/sub/d1");
            Directory.CreateDirectory(path + "/ShadowTests/sub/d2");

            var files = fs.GetFiles("");
            Assert.AreEqual(0, files.Count());

            var dirs = ss.GetDirectories("");
            Assert.AreEqual(1, dirs.Count());
            Assert.IsTrue(dirs.Contains("sub"));

            dirs = fs.GetDirectories("sub");
            Assert.AreEqual(2, dirs.Count());
            Assert.IsTrue(dirs.Contains("sub/d1"));
            Assert.IsTrue(dirs.Contains("sub/d2"));

            dirs = ss.GetDirectories("sub");
            Assert.AreEqual(2, dirs.Count());
            Assert.IsTrue(dirs.Contains("sub/d1"));
            Assert.IsTrue(dirs.Contains("sub/d2"));

            ss.DeleteDirectory("sub/d1");

            Assert.IsTrue(Directory.Exists(path + "/ShadowTests/sub/d1"));
            Assert.IsTrue(fs.DirectoryExists("sub/d1"));
            Assert.IsFalse(ss.DirectoryExists("sub/d1"));

            dirs = fs.GetDirectories("sub");
            Assert.AreEqual(2, dirs.Count());
            Assert.IsTrue(dirs.Contains("sub/d1"));
            Assert.IsTrue(dirs.Contains("sub/d2"));

            dirs = ss.GetDirectories("sub");
            Assert.AreEqual(1, dirs.Count());
            Assert.IsTrue(dirs.Contains("sub/d2"));
        }

        [Test]
        public void ShadowDeleteFile()
        {
            var path = IOHelper.MapPath("FileSysTests");
            Directory.CreateDirectory(path);
            Directory.CreateDirectory(path + "/ShadowTests");
            Directory.CreateDirectory(path + "/ShadowSystem");

            var fs = new PhysicalFileSystem(path + "/ShadowTests/", "ignore");
            var sfs = new PhysicalFileSystem(path + "/ShadowSystem/", "ignore");
            var ss = new ShadowFileSystem(fs, sfs);

            File.WriteAllText(path + "/ShadowTests/f1.txt", "foo");
            File.WriteAllText(path + "/ShadowTests/f2.txt", "foo");

            var files = fs.GetFiles("");
            Assert.AreEqual(2, files.Count());
            Assert.IsTrue(files.Contains("f1.txt"));
            Assert.IsTrue(files.Contains("f2.txt"));

            files = ss.GetFiles("");
            Assert.AreEqual(2, files.Count());
            Assert.IsTrue(files.Contains("f1.txt"));
            Assert.IsTrue(files.Contains("f2.txt"));

            var dirs = ss.GetDirectories("");
            Assert.AreEqual(0, dirs.Count());

            ss.DeleteFile("f1.txt");

            Assert.IsTrue(File.Exists(path + "/ShadowTests/f1.txt"));
            Assert.IsTrue(fs.FileExists("f1.txt"));
            Assert.IsFalse(ss.FileExists("f1.txt"));

            files = ss.GetFiles("");
            Assert.AreEqual(1, files.Count());
            Assert.IsTrue(files.Contains("f2.txt"));
        }

        [Test]
        public void ShadowDeleteFileInDir()
        {
            var path = IOHelper.MapPath("FileSysTests");
            Directory.CreateDirectory(path);
            Directory.CreateDirectory(path + "/ShadowTests");
            Directory.CreateDirectory(path + "/ShadowSystem");

            var fs = new PhysicalFileSystem(path + "/ShadowTests/", "ignore");
            var sfs = new PhysicalFileSystem(path + "/ShadowSystem/", "ignore");
            var ss = new ShadowFileSystem(fs, sfs);

            Directory.CreateDirectory(path + "/ShadowTests/sub");
            File.WriteAllText(path + "/ShadowTests/sub/f1.txt", "foo");
            File.WriteAllText(path + "/ShadowTests/sub/f2.txt", "foo");

            var files = fs.GetFiles("");
            Assert.AreEqual(0, files.Count());

            files = fs.GetFiles("sub");
            Assert.AreEqual(2, files.Count());
            Assert.IsTrue(files.Contains("sub/f1.txt"));
            Assert.IsTrue(files.Contains("sub/f2.txt"));

            files = ss.GetFiles("");
            Assert.AreEqual(0, files.Count());

            var dirs = ss.GetDirectories("");
            Assert.AreEqual(1, dirs.Count());
            Assert.IsTrue(dirs.Contains("sub"));

            files = ss.GetFiles("sub");
            Assert.AreEqual(2, files.Count());
            Assert.IsTrue(files.Contains("sub/f1.txt"));
            Assert.IsTrue(files.Contains("sub/f2.txt"));

            dirs = ss.GetDirectories("sub");
            Assert.AreEqual(0, dirs.Count());

            ss.DeleteFile("sub/f1.txt");

            Assert.IsTrue(File.Exists(path + "/ShadowTests/sub/f1.txt"));
            Assert.IsTrue(fs.FileExists("sub/f1.txt"));
            Assert.IsFalse(ss.FileExists("sub/f1.txt"));

            files = fs.GetFiles("sub");
            Assert.AreEqual(2, files.Count());
            Assert.IsTrue(files.Contains("sub/f1.txt"));
            Assert.IsTrue(files.Contains("sub/f2.txt"));

            files = ss.GetFiles("sub");
            Assert.AreEqual(1, files.Count());
            Assert.IsTrue(files.Contains("sub/f2.txt"));
        }

        [Test]
        public void ShadowCantCreateFile()
        {
            var path = IOHelper.MapPath("FileSysTests");
            Directory.CreateDirectory(path);
            Directory.CreateDirectory(path + "/ShadowTests");
            Directory.CreateDirectory(path + "/ShadowSystem");

            var fs = new PhysicalFileSystem(path + "/ShadowTests/", "ignore");
            var sfs = new PhysicalFileSystem(path + "/ShadowSystem/", "ignore");
            var ss = new ShadowFileSystem(fs, sfs);

            Assert.Throws<UnauthorizedAccessException>(() =>
            {
                using (var ms = new MemoryStream(Encoding.UTF8.GetBytes("foo")))
                    ss.AddFile("../../f1.txt", ms);
            });
        }

        [Test]
        public void ShadowCreateFile()
        {
            var path = IOHelper.MapPath("FileSysTests");
            Directory.CreateDirectory(path);
            Directory.CreateDirectory(path + "/ShadowTests");
            Directory.CreateDirectory(path + "/ShadowSystem");

            var fs = new PhysicalFileSystem(path + "/ShadowTests/", "ignore");
            var sfs = new PhysicalFileSystem(path + "/ShadowSystem/", "ignore");
            var ss = new ShadowFileSystem(fs, sfs);

            File.WriteAllText(path + "/ShadowTests/f2.txt", "foo");

            using (var ms = new MemoryStream(Encoding.UTF8.GetBytes("foo")))
                ss.AddFile("f1.txt", ms);

            Assert.IsTrue(File.Exists(path + "/ShadowTests/f2.txt"));
            Assert.IsFalse(File.Exists(path + "/ShadowSystem/f2.txt"));
            Assert.IsTrue(fs.FileExists("f2.txt"));
            Assert.IsTrue(ss.FileExists("f2.txt"));

            Assert.IsFalse(File.Exists(path + "/ShadowTests/f1.txt"));
            Assert.IsTrue(File.Exists(path + "/ShadowSystem/f1.txt"));
            Assert.IsFalse(fs.FileExists("f1.txt"));
            Assert.IsTrue(ss.FileExists("f1.txt"));

            var files = ss.GetFiles("");
            Assert.AreEqual(2, files.Count());
            Assert.IsTrue(files.Contains("f1.txt"));
            Assert.IsTrue(files.Contains("f2.txt"));

            string content;
            using (var stream = ss.OpenFile("f1.txt"))
                content = new StreamReader(stream).ReadToEnd();

            Assert.AreEqual("foo", content);
        }

        [Test]
        public void ShadowCreateFileInDir()
        {
            var path = IOHelper.MapPath("FileSysTests");
            Directory.CreateDirectory(path);
            Directory.CreateDirectory(path + "/ShadowTests");
            Directory.CreateDirectory(path + "/ShadowSystem");

            var fs = new PhysicalFileSystem(path + "/ShadowTests/", "ignore");
            var sfs = new PhysicalFileSystem(path + "/ShadowSystem/", "ignore");
            var ss = new ShadowFileSystem(fs, sfs);

            using (var ms = new MemoryStream(Encoding.UTF8.GetBytes("foo")))
                ss.AddFile("sub/f1.txt", ms);

            Assert.IsFalse(File.Exists(path + "/ShadowTests/sub/f1.txt"));
            Assert.IsTrue(File.Exists(path + "/ShadowSystem/sub/f1.txt"));
            Assert.IsFalse(fs.FileExists("sub/f1.txt"));
            Assert.IsTrue(ss.FileExists("sub/f1.txt"));

            Assert.IsFalse(fs.DirectoryExists("sub"));
            Assert.IsTrue(ss.DirectoryExists("sub"));

            var dirs = fs.GetDirectories("");
            Assert.AreEqual(0, dirs.Count());

            dirs = ss.GetDirectories("");
            Assert.AreEqual(1, dirs.Count());
            Assert.IsTrue(dirs.Contains("sub"));

            var files = ss.GetFiles("sub");
            Assert.AreEqual(1, files.Count());

            string content;
            using (var stream = ss.OpenFile("sub/f1.txt"))
                content = new StreamReader(stream).ReadToEnd();

            Assert.AreEqual("foo", content);
        }

        [Test]
        public void ShadowAbort()
        {
            var path = IOHelper.MapPath("FileSysTests");
            Directory.CreateDirectory(path);
            Directory.CreateDirectory(path + "/ShadowTests");
            Directory.CreateDirectory(path + "/ShadowSystem");

            var fs = new PhysicalFileSystem(path + "/ShadowTests/", "ignore");
            var sfs = new PhysicalFileSystem(path + "/ShadowSystem/", "ignore");
            var ss = new ShadowFileSystem(fs, sfs);

            using (var ms = new MemoryStream(Encoding.UTF8.GetBytes("foo")))
                ss.AddFile("path/to/some/dir/f1.txt", ms);

            // file is only written to the shadow fs
            Assert.IsTrue(File.Exists(path + "/ShadowSystem/path/to/some/dir/f1.txt"));
            Assert.IsFalse(File.Exists(path + "/ShadowTests/path/to/some/dir/f1.txt"));

            // let the shadow fs die
        }

        [Test]
        public void ShadowComplete()
        {
            var path = IOHelper.MapPath("FileSysTests");
            Directory.CreateDirectory(path);
            Directory.CreateDirectory(path + "/ShadowTests");
            Directory.CreateDirectory(path + "/ShadowSystem");

            var fs = new PhysicalFileSystem(path + "/ShadowTests/", "ignore");
            var sfs = new PhysicalFileSystem(path + "/ShadowSystem/", "ignore");
            var ss = new ShadowFileSystem(fs, sfs);

            Directory.CreateDirectory(path + "/ShadowTests/sub/sub");
            File.WriteAllText(path + "/ShadowTests/sub/sub/f2.txt", "foo");

            using (var ms = new MemoryStream(Encoding.UTF8.GetBytes("foo")))
                ss.AddFile("path/to/some/dir/f1.txt", ms);
            ss.DeleteFile("sub/sub/f2.txt");

            Assert.IsTrue(File.Exists(path + "/ShadowSystem/path/to/some/dir/f1.txt"));

            ss.Complete();

            // yes we are cleaning now
            //Assert.IsTrue(File.Exists(path + "/ShadowSystem/path/to/some/dir/f1.txt")); // *not* cleaning
            Assert.IsTrue(File.Exists(path + "/ShadowTests/path/to/some/dir/f1.txt"));
            Assert.IsFalse(File.Exists(path + "/ShadowTests/sub/sub/f2.txt"));
        }

        class FS : FileSystemWrapper
        {
            public FS(IFileSystem innerFileSystem)
                : base(innerFileSystem)
            { }
        }

        [Test]
        public void ShadowScopeComplete()
        {
            var logger = Mock.Of<ILogger>();

            var path = IOHelper.MapPath("FileSysTests");
            var shadowfs = IOHelper.MapPath(SystemDirectories.TempData.EnsureEndsWith('/') + "ShadowFs");
            Directory.CreateDirectory(path);
            Directory.CreateDirectory(shadowfs);

            var scopedFileSystems = false;

            var phy = new PhysicalFileSystem(path, "ignore");

            var container = Mock.Of<IFactory>();
            var fileSystems = new FileSystems(container, logger) { IsScoped = () => scopedFileSystems };
            var fs = fileSystems.GetFileSystem<FS>(phy);
            var sw = (ShadowWrapper) fs.InnerFileSystem;

            using (var ms = new MemoryStream(Encoding.UTF8.GetBytes("foo")))
                sw.AddFile("sub/f1.txt", ms);
            Assert.IsTrue(phy.FileExists("sub/f1.txt"));

            string id;

            // explicit shadow without scope does not work
            sw.Shadow(id = ShadowWrapper.CreateShadowId());
            Assert.IsTrue(Directory.Exists(shadowfs + "/" + id));
            using (var ms = new MemoryStream(Encoding.UTF8.GetBytes("foo")))
                sw.AddFile("sub/f2.txt", ms);
            Assert.IsTrue(phy.FileExists("sub/f2.txt"));
            sw.UnShadow(true);
            Assert.IsTrue(phy.FileExists("sub/f2.txt"));
            Assert.IsFalse(Directory.Exists(shadowfs + "/" + id));

            // shadow with scope but no complete does not complete
            scopedFileSystems = true; // pretend we have a scope
            var scope = new ShadowFileSystems(fileSystems, id = ShadowWrapper.CreateShadowId());
            Assert.IsTrue(Directory.Exists(shadowfs + "/" + id));
            using (var ms = new MemoryStream(Encoding.UTF8.GetBytes("foo")))
                sw.AddFile("sub/f3.txt", ms);
            Assert.IsFalse(phy.FileExists("sub/f3.txt"));
            var dirs = Directory.GetDirectories(shadowfs);
            Assert.AreEqual(1, dirs.Length);
            Assert.AreEqual((shadowfs + "/" + id).Replace('\\', '/'), dirs[0].Replace('\\', '/'));
            dirs = Directory.GetDirectories(dirs[0]);
            var typedDir = dirs.FirstOrDefault(x => x.Replace('\\', '/').EndsWith("/x"));
            Assert.IsNotNull(typedDir);
            dirs = Directory.GetDirectories(typedDir);
            var suid = fileSystems.Paths[typeof(FS)];
            var scopedDir = dirs.FirstOrDefault(x => x.Replace('\\', '/').EndsWith("/" + suid)); // this is where files go
            Assert.IsNotNull(scopedDir);
            scope.Dispose();
            scopedFileSystems = false;
            Assert.IsFalse(phy.FileExists("sub/f3.txt"));
            TestHelper.TryAssert(() => Assert.IsFalse(Directory.Exists(shadowfs + "/" + id)));

            // shadow with scope and complete does complete
            scopedFileSystems = true; // pretend we have a scope
            scope = new ShadowFileSystems(fileSystems, id = ShadowWrapper.CreateShadowId());
            Assert.IsTrue(Directory.Exists(shadowfs + "/" + id));
            using (var ms = new MemoryStream(Encoding.UTF8.GetBytes("foo")))
                sw.AddFile("sub/f4.txt", ms);
            Assert.IsFalse(phy.FileExists("sub/f4.txt"));
            Assert.AreEqual(1, Directory.GetDirectories(shadowfs).Length);
            scope.Complete();
            scope.Dispose();
            scopedFileSystems = false;
            TestHelper.TryAssert(() => Assert.AreEqual(0, Directory.GetDirectories(shadowfs).Length));
            Assert.IsTrue(phy.FileExists("sub/f4.txt"));
            Assert.IsFalse(Directory.Exists(shadowfs + "/" + id));

            // test scope for "another thread"

            scopedFileSystems = true; // pretend we have a scope
            scope = new ShadowFileSystems(fileSystems, id = ShadowWrapper.CreateShadowId());
            Assert.IsTrue(Directory.Exists(shadowfs + "/" + id));
            using (var ms = new MemoryStream(Encoding.UTF8.GetBytes("foo")))
                sw.AddFile("sub/f5.txt", ms);
            Assert.IsFalse(phy.FileExists("sub/f5.txt"));

            // pretend we're another thread w/out scope
            scopedFileSystems = false;
            using (var ms = new MemoryStream(Encoding.UTF8.GetBytes("foo")))
                sw.AddFile("sub/f6.txt", ms);
            scopedFileSystems = true; // pretend we have a scope

            Assert.IsTrue(phy.FileExists("sub/f6.txt")); // other thread has written out to fs
            scope.Complete();
            scope.Dispose();
            scopedFileSystems = false;
            Assert.IsTrue(phy.FileExists("sub/f5.txt"));
            TestHelper.TryAssert(() => Assert.IsFalse(Directory.Exists(shadowfs + "/" + id)));
        }

        [Test]
        public void ShadowScopeCompleteWithFileConflict()
        {
            var logger = Mock.Of<ILogger>();

            var path = IOHelper.MapPath("FileSysTests");
            var shadowfs = IOHelper.MapPath(SystemDirectories.TempData.EnsureEndsWith('/') + "ShadowFs");
            Directory.CreateDirectory(path);

            var scopedFileSystems = false;

            var phy = new PhysicalFileSystem(path, "ignore");

            var container = Mock.Of<IFactory>();
            var fileSystems = new FileSystems(container, logger) { IsScoped = () => scopedFileSystems };
            var fs = fileSystems.GetFileSystem<FS>( phy);
            var sw = (ShadowWrapper) fs.InnerFileSystem;

            using (var ms = new MemoryStream(Encoding.UTF8.GetBytes("foo")))
                sw.AddFile("sub/f1.txt", ms);
            Assert.IsTrue(phy.FileExists("sub/f1.txt"));

            string id;

            scopedFileSystems = true; // pretend we have a scope
            var scope = new ShadowFileSystems(fileSystems, id = ShadowWrapper.CreateShadowId());
            Assert.IsTrue(Directory.Exists(shadowfs + "/" + id));
            using (var ms = new MemoryStream(Encoding.UTF8.GetBytes("foo")))
                sw.AddFile("sub/f2.txt", ms);
            Assert.IsFalse(phy.FileExists("sub/f2.txt"));

            // pretend we're another thread w/out scope
            scopedFileSystems = false;
            using (var ms = new MemoryStream(Encoding.UTF8.GetBytes("bar")))
                sw.AddFile("sub/f2.txt", ms);
            scopedFileSystems = true; // pretend we have a scope

            Assert.IsTrue(phy.FileExists("sub/f2.txt")); // other thread has written out to fs
            scope.Complete();
            scope.Dispose();
            scopedFileSystems = false;
            Assert.IsTrue(phy.FileExists("sub/f2.txt"));
            TestHelper.TryAssert(() => Assert.IsFalse(Directory.Exists(shadowfs + "/" + id)));

            string text;
            using (var s = phy.OpenFile("sub/f2.txt"))
            using (var r = new StreamReader(s))
                text = r.ReadToEnd();

            // the shadow filesystem will happily overwrite anything it can
            Assert.AreEqual("foo", text);
        }

        [Test]
        public void ShadowScopeCompleteWithDirectoryConflict()
        {
            var logger = Mock.Of<ILogger>();

            var path = IOHelper.MapPath("FileSysTests");
            var shadowfs = IOHelper.MapPath(SystemDirectories.TempData.EnsureEndsWith('/') + "ShadowFs");
            Directory.CreateDirectory(path);

            var scopedFileSystems = false;

            var phy = new PhysicalFileSystem(path, "ignore");

            var container = Mock.Of<IFactory>();
            var fileSystems = new FileSystems(container, logger) { IsScoped = () => scopedFileSystems };
            var fs = fileSystems.GetFileSystem<FS>( phy);
            var sw = (ShadowWrapper)fs.InnerFileSystem;

            using (var ms = new MemoryStream(Encoding.UTF8.GetBytes("foo")))
                sw.AddFile("sub/f1.txt", ms);
            Assert.IsTrue(phy.FileExists("sub/f1.txt"));

            string id;

            scopedFileSystems = true; // pretend we have a scope
            var scope = new ShadowFileSystems(fileSystems, id = ShadowWrapper.CreateShadowId());
            Assert.IsTrue(Directory.Exists(shadowfs + "/" + id));
            using (var ms = new MemoryStream(Encoding.UTF8.GetBytes("foo")))
                sw.AddFile("sub/f2.txt", ms);
            Assert.IsFalse(phy.FileExists("sub/f2.txt"));

            // pretend we're another thread w/out scope
            scopedFileSystems = false;
            using (var ms = new MemoryStream(Encoding.UTF8.GetBytes("bar")))
                sw.AddFile("sub/f2.txt/f2.txt", ms);
            scopedFileSystems = true; // pretend we have a scope

            Assert.IsTrue(phy.FileExists("sub/f2.txt/f2.txt")); // other thread has written out to fs

            using (var ms = new MemoryStream(Encoding.UTF8.GetBytes("foo")))
                sw.AddFile("sub/f3.txt", ms);
            Assert.IsFalse(phy.FileExists("sub/f3.txt"));

            scope.Complete();

            try
            {
                // no way this can work since we're trying to write a file
                // but there's now a directory with the same name on the real fs
                scope.Dispose();
                Assert.Fail("Expected AggregateException.");
            }
            catch (AggregateException ae)
            {
                Assert.AreEqual(1, ae.InnerExceptions.Count);
                var e = ae.InnerExceptions[0];
                Assert.IsNotNull(e.InnerException);
                Assert.IsInstanceOf<AggregateException>(e);
                ae = (AggregateException) e;

                Assert.AreEqual(1, ae.InnerExceptions.Count);
                e = ae.InnerExceptions[0];
                Assert.IsNotNull(e.InnerException);
                Assert.IsInstanceOf<Exception>(e.InnerException);
            }

            // still, the rest of the changes has been applied ok
            Assert.IsTrue(phy.FileExists("sub/f3.txt"));
        }

        [Test]
        public void GetFilesReturnsChildrenOnly()
        {
            var path = IOHelper.MapPath("FileSysTests");
            Directory.CreateDirectory(path);
            File.WriteAllText(path + "/f1.txt", "foo");
            Directory.CreateDirectory(path + "/test");
            File.WriteAllText(path + "/test/f2.txt", "foo");
            Directory.CreateDirectory(path + "/test/inner");
            File.WriteAllText(path + "/test/inner/f3.txt", "foo");

            path = NormPath(path);
            var files = Directory.GetFiles(path);
            Assert.AreEqual(1, files.Length);
            files = Directory.GetFiles(path, "*", SearchOption.AllDirectories);
            Assert.AreEqual(3, files.Length);
            var efiles = Directory.EnumerateFiles(path);
            Assert.AreEqual(1, efiles.Count());
            efiles = Directory.EnumerateFiles(path, "*", SearchOption.AllDirectories);
            Assert.AreEqual(3, efiles.Count());
        }

        [Test]
        public void DeleteDirectoryAndFiles()
        {
            var path = IOHelper.MapPath("FileSysTests");
            Directory.CreateDirectory(path);
            File.WriteAllText(path + "/f1.txt", "foo");
            Directory.CreateDirectory(path + "/test");
            File.WriteAllText(path + "/test/f2.txt", "foo");
            Directory.CreateDirectory(path + "/test/inner");
            File.WriteAllText(path + "/test/inner/f3.txt", "foo");

            path = NormPath(path);
            TestHelper.Try(() => Directory.Delete(path, true));
            TestHelper.TryAssert(() => Assert.IsFalse(File.Exists(path + "/test/inner/f3.txt")));
        }

        /// <summary>
        /// Check that GetFiles will return all files on the shadow, while returning
        /// just one on each of the filesystems used by the shadow.
        /// </summary>
        [Test]
        public void ShadowGetFiles()
        {
            // Arrange
            var path = IOHelper.MapPath("FileSysTests");
            Directory.CreateDirectory(path);
            Directory.CreateDirectory(path + "/ShadowTests");
            Directory.CreateDirectory(path + "/ShadowSystem");

            var fs = new PhysicalFileSystem(path + "/ShadowTests/", "ignore");
            var sfs = new PhysicalFileSystem(path + "/ShadowSystem/", "ignore");
            var ss = new ShadowFileSystem(fs, sfs);

            // Act
            File.WriteAllText(path + "/ShadowTests/f2.txt", "foo");
            using (var ms = new MemoryStream(Encoding.UTF8.GetBytes("foo")))
                ss.AddFile("f1.txt", ms);

            // Assert
            // ensure we get 2 files from the shadow
            var getFiles = ss.GetFiles(string.Empty);
            Assert.AreEqual(2, getFiles.Count());

            var fsFiles = fs.GetFiles(string.Empty).ToArray();
            Assert.AreEqual(1, fsFiles.Length);
            var sfsFiles = sfs.GetFiles(string.Empty).ToArray();
            Assert.AreEqual(1, sfsFiles.Length);
        }

        /// <summary>
        /// Check that GetFiles using the filter function with empty string will return expected results
        /// </summary>
        [Test]
        public void ShadowGetFilesUsingEmptyFilter()
        {
            // Arrange
            var path = IOHelper.MapPath("FileSysTests");
            Directory.CreateDirectory(path);
            Directory.CreateDirectory(path + "/ShadowTests");
            Directory.CreateDirectory(path + "/ShadowSystem");

            var fs = new PhysicalFileSystem(path + "/ShadowTests/", "ignore");
            var sfs = new PhysicalFileSystem(path + "/ShadowSystem/", "ignore");
            var ss = new ShadowFileSystem(fs, sfs);

            // Act
            File.WriteAllText(path + "/ShadowTests/f2.txt", "foo");
            using (var ms = new MemoryStream(Encoding.UTF8.GetBytes("foo")))
                ss.AddFile("f1.txt", ms);

            // Assert
            // ensure we get 2 files from the shadow
            var getFiles = ss.GetFiles(string.Empty);
            Assert.AreEqual(2, getFiles.Count());
            // ensure we get 0 files when using a empty filter
            var getFilesWithEmptyFilter = ss.GetFiles(string.Empty, "");
            Assert.AreEqual(0, getFilesWithEmptyFilter.Count());

            var fsFiles = fs.GetFiles(string.Empty).ToArray();
            Assert.AreEqual(1, fsFiles.Length);
            var sfsFiles = sfs.GetFiles(string.Empty).ToArray();
            Assert.AreEqual(1, sfsFiles.Length);
        }

        /// <summary>
        /// Check that GetFiles using the filter function with null will return expected results
        /// </summary>
        [Test]
        public void ShadowGetFilesUsingNullFilter()
        {
            // Arrange
            var path = IOHelper.MapPath("FileSysTests");
            Directory.CreateDirectory(path);
            Directory.CreateDirectory(path + "/ShadowTests");
            Directory.CreateDirectory(path + "/ShadowSystem");

            var fs = new PhysicalFileSystem(path + "/ShadowTests/", "ignore");
            var sfs = new PhysicalFileSystem(path + "/ShadowSystem/", "ignore");
            var ss = new ShadowFileSystem(fs, sfs);

            // Act
            File.WriteAllText(path + "/ShadowTests/f2.txt", "foo");
            using (var ms = new MemoryStream(Encoding.UTF8.GetBytes("foo")))
                ss.AddFile("f1.txt", ms);

            // Assert
            // ensure we get 2 files from the shadow
            var getFiles = ss.GetFiles(string.Empty);
            Assert.AreEqual(2, getFiles.Count());
            // ensure we get 2 files when using null in filter parameter
            var getFilesWithNullFilter = ss.GetFiles(string.Empty, null);
            Assert.AreEqual(2, getFilesWithNullFilter.Count());

            var fsFiles = fs.GetFiles(string.Empty).ToArray();
            Assert.AreEqual(1, fsFiles.Length);
            var sfsFiles = sfs.GetFiles(string.Empty).ToArray();
            Assert.AreEqual(1, sfsFiles.Length);
        }

        [Test]
        public void ShadowGetFilesUsingWildcardFilter()
        {
            // Arrange
            var path = IOHelper.MapPath("FileSysTests");
            Directory.CreateDirectory(path);
            Directory.CreateDirectory(path + "/ShadowTests");
            Directory.CreateDirectory(path + "/ShadowSystem");

            var fs = new PhysicalFileSystem(path + "/ShadowTests/", "ignore");
            var sfs = new PhysicalFileSystem(path + "/ShadowSystem/", "ignore");
            var ss = new ShadowFileSystem(fs, sfs);

            // Act
            File.WriteAllText(path + "/ShadowTests/f2.txt", "foo");
            File.WriteAllText(path + "/ShadowTests/f2.doc", "foo");
            using (var ms = new MemoryStream(Encoding.UTF8.GetBytes("foo")))
                ss.AddFile("f1.txt", ms);
            using (var ms = new MemoryStream(Encoding.UTF8.GetBytes("foo")))
                ss.AddFile("f1.doc", ms);

            // Assert
            // ensure we get 4 files from the shadow
            var getFiles = ss.GetFiles(string.Empty);
            Assert.AreEqual(4, getFiles.Count());
            // ensure we get only 2 of 4 files from the shadow when using filter
            var getFilesWithWildcardFilter = ss.GetFiles(string.Empty, "*.doc");
            Assert.AreEqual(2, getFilesWithWildcardFilter.Count());

            var fsFiles = fs.GetFiles(string.Empty).ToArray();
            Assert.AreEqual(2, fsFiles.Length);
            var sfsFiles = sfs.GetFiles(string.Empty).ToArray();
            Assert.AreEqual(2, sfsFiles.Length);
        }

        [Test]
        public void ShadowGetFilesUsingSingleCharacterFilter()
        {
            // Arrange
            var path = IOHelper.MapPath("FileSysTests");
            Directory.CreateDirectory(path);
            Directory.CreateDirectory(path + "/ShadowTests");
            Directory.CreateDirectory(path + "/ShadowSystem");

            var fs = new PhysicalFileSystem(path + "/ShadowTests/", "ignore");
            var sfs = new PhysicalFileSystem(path + "/ShadowSystem/", "ignore");
            var ss = new ShadowFileSystem(fs, sfs);

            // Act
            File.WriteAllText(path + "/ShadowTests/f2.txt", "foo");
            File.WriteAllText(path + "/ShadowTests/f2.doc", "foo");
            File.WriteAllText(path + "/ShadowTests/f2.docx", "foo");
            using (var ms = new MemoryStream(Encoding.UTF8.GetBytes("foo")))
                ss.AddFile("f1.txt", ms);
            using (var ms = new MemoryStream(Encoding.UTF8.GetBytes("foo")))
                ss.AddFile("f1.doc", ms);
            using (var ms = new MemoryStream(Encoding.UTF8.GetBytes("foo")))
                ss.AddFile("f1.docx", ms);

            // Assert
            // ensure we get 6 files from the shadow
            var getFiles = ss.GetFiles(string.Empty);
            Assert.AreEqual(6, getFiles.Count());
            // ensure we get only 2 of 6 files from the shadow when using filter on shadow
            var getFilesWithWildcardSinglecharFilter = ss.GetFiles(string.Empty, "f1.d?c");
            Assert.AreEqual(1, getFilesWithWildcardSinglecharFilter.Count());
            // ensure we get only 2 of 6 files from the shadow when using filter on disk
            var getFilesWithWildcardSinglecharFilter2 = ss.GetFiles(string.Empty, "f2.d?c");
            Assert.AreEqual(1, getFilesWithWildcardSinglecharFilter2.Count());

            var fsFiles = fs.GetFiles(string.Empty).ToArray();
            Assert.AreEqual(3, fsFiles.Length);
            var sfsFiles = sfs.GetFiles(string.Empty).ToArray();
            Assert.AreEqual(3, sfsFiles.Length);
        }

        /// <summary>
        /// Returns the full paths of the files on the disk.
        /// Note that this will be the *actual* path of the file, meaning a file existing on the initialized FS
        /// will be in one location, while a file written after initializing the shadow, will exist at the
        /// shadow location directory.
        /// </summary>
        [Test]
        public void ShadowGetFullPath()
        {
            // Arrange
            var path = IOHelper.MapPath("FileSysTests");
            Directory.CreateDirectory(path);
            Directory.CreateDirectory(path + "/ShadowTests");
            Directory.CreateDirectory(path + "/ShadowSystem");

            var fs = new PhysicalFileSystem(path + "/ShadowTests/", "ignore");
            var sfs = new PhysicalFileSystem(path + "/ShadowSystem/", "ignore");
            var ss = new ShadowFileSystem(fs, sfs);

            // Act
            File.WriteAllText(path + "/ShadowTests/f1.txt", "foo");
            using (var ms = new MemoryStream(Encoding.UTF8.GetBytes("foo")))
                ss.AddFile("f2.txt", ms);

            // Assert
            var f1FullPath = ss.GetFullPath("f1.txt");
            var f2FullPath = ss.GetFullPath("f2.txt");
            Assert.AreEqual(Path.Combine(path, "ShadowTests", "f1.txt"), f1FullPath);
            Assert.AreEqual(Path.Combine(path, "ShadowSystem", "f2.txt"), f2FullPath);
        }

        /// <summary>
        /// Returns the path relative to the filesystem root
        /// </summary>
        /// <remarks>
        /// This file stuff in this test is kinda irrelevant with the current implementation.
        /// We do tests that the files are written to the correct places and the relative path is returned correct,
        /// but GetRelativePath is currently really just string manipulation so files are not actually hit by the code.
        /// Leaving the file stuff in here for now in case the method becomes more clever at some point.
        /// </remarks>
        [Test]
        public void ShadowGetRelativePath()
        {
            // Arrange
            var path = IOHelper.MapPath("FileSysTests");
            Directory.CreateDirectory(path);
            Directory.CreateDirectory(path + "/ShadowTests");
            Directory.CreateDirectory(path + "/ShadowSystem");

            var fs = new PhysicalFileSystem(path + "/ShadowTests/", "ignore");
            var sfs = new PhysicalFileSystem(path + "/ShadowSystem/", "ignore");
            var ss = new ShadowFileSystem(fs, sfs);

            // Act
            File.WriteAllText(path + "/ShadowTests/f1.txt", "foo");
            using (var ms = new MemoryStream(Encoding.UTF8.GetBytes("foo")))
                ss.AddFile("f2.txt", ms);

            // Assert
            var f1RelativePath = ss.GetRelativePath("f1.txt");
            var f2RelativePath = ss.GetRelativePath("f2.txt");
            Assert.AreEqual("f1.txt", f1RelativePath);
            Assert.AreEqual("f2.txt", f2RelativePath);
            Assert.IsTrue(File.Exists(Path.Combine(path, "ShadowTests", "f1.txt")));
            Assert.IsFalse(File.Exists(Path.Combine(path, "ShadowTests", "f2.txt")));
            Assert.IsTrue(File.Exists(Path.Combine(path, "ShadowSystem", "f2.txt")));
            Assert.IsFalse(File.Exists(Path.Combine(path, "ShadowSystem", "f1.txt")));
        }

        /// <summary>
        /// Ensure the url returned contains the path relative to the FS root,
        /// but including the rootUrl the FS was initialized with.
        /// </summary>
        /// <remarks>
        /// This file stuff in this test is kinda irrelevant with the current implementation.
        /// We do tests that the files are written to the correct places and the url is returned correct,
        /// but GetUrl is currently really just string manipulation so files are not actually hit by the code.
        /// Leaving the file stuff in here for now in case the method becomes more clever at some point.
        /// </remarks>
        [Test]
        public void ShadowGetUrl()
        {
            // Arrange
            var path = IOHelper.MapPath("FileSysTests");
            Directory.CreateDirectory(path);
            Directory.CreateDirectory(path + "/ShadowTests");
            Directory.CreateDirectory(path + "/ShadowSystem");

            var fs = new PhysicalFileSystem(path + "/ShadowTests/", "rootUrl");
            var sfs = new PhysicalFileSystem(path + "/ShadowSystem/", "rootUrl");
            var ss = new ShadowFileSystem(fs, sfs);

            // Act
            File.WriteAllText(path + "/ShadowTests/f1.txt", "foo");
            using (var ms = new MemoryStream(Encoding.UTF8.GetBytes("foo")))
                ss.AddFile("f2.txt", ms);

            // Assert
            var f1Url = ss.GetUrl("f1.txt");
            var f2Url = ss.GetUrl("f2.txt");
            Assert.AreEqual("rootUrl/f1.txt", f1Url);
            Assert.AreEqual("rootUrl/f2.txt", f2Url);
            Assert.IsTrue(File.Exists(Path.Combine(path, "ShadowTests", "f1.txt")));
            Assert.IsFalse(File.Exists(Path.Combine(path, "ShadowTests", "f2.txt")));
            Assert.IsTrue(File.Exists(Path.Combine(path, "ShadowSystem", "f2.txt")));
            Assert.IsFalse(File.Exists(Path.Combine(path, "ShadowSystem", "f1.txt")));
        }
    }
}


text:
set :stages, %w(vagrant staging production)
set :default_stage, "staging"

require 'capistrano/ext/multistage'
require 'bundler/capistrano'
require 'capistrano-notification'
require 'honeybadger/capistrano'

notification.irc do |irc|
  irc.host    'chat.freenode.net'
  irc.channel '#rubygems'
  irc.message { "Deployed rubygems.org @ https://github.com/rubygems/rubygems.org/commit/#{fetch(:current_revision)} to #{stage} (#{roles[:app].servers.compact.map(&:host).join(', ')})" }
end

default_run_options[:pty] = true
set :ssh_options, { :forward_agent => true }
set :application, "rubygems"
set(:rails_env) { "#{stage}"}
set :deploy_to, "/applications/rubygems"
set :bundle_cmd, "/usr/local/bin/bundle"
set :scm, :git
set :repository, "https://github.com/rubygems/rubygems.org.git"
set :repository_cache, "git_cache"
set :deploy_via, :remote_cache
set :git_shallow_clone, 1
set :git_enable_submodules, 1
set :use_sudo, false
set :group, "deploy"
set :assets_role, [:app]

after "deploy", "deploy:migrate"
after "deploy", "deploy:cleanup"
after "deploy:finalize_update", "deploy:symlink_database_yml", "deploy:symlink_secret_settings"

namespace :deploy do

  desc "Remove git cache for clean deploy"
  task :clean_git_cache, :roles => :app do
    run "rm -rf #{shared_path}/#{repository_cache}"
  end

  desc "Symlink database.yml for this environment"
  task :symlink_database_yml, :roles => :app do
    run "ln -fs #{shared_path}/database.yml #{release_path}/config/database.yml"
  end

  desc "Symlink secret settings for this environment"
  task :symlink_secret_settings, :roles => :app do
    run "ln -fs #{shared_path}/secret.rb #{release_path}/config/secret.rb"
  end

  desc "Restart unicorn and delayed_job"
  task :restart, :roles => :restart do
    sudo "service unicorn restart"
    sudo "service delayed_job restart"
  end

end


text:
[![Build Status](https://github.com/johnbywater/eventsourcing/actions/workflows/runtests.yaml/badge.svg?branch=9.1)](https://github.com/johnbywater/eventsourcing/tree/9.1)
[![Coverage Status](https://coveralls.io/repos/github/johnbywater/eventsourcing/badge.svg?branch=9.1)](https://coveralls.io/github/johnbywater/eventsourcing?branch=9.1)
[![Documentation Status](https://readthedocs.org/projects/eventsourcing/badge/?version=v9.1.4)](https://eventsourcing.readthedocs.io/en/v9.1.4/)
[![Latest Release](https://badge.fury.io/py/eventsourcing.svg)](https://pypi.org/project/eventsourcing/)

# Event Sourcing in Python

A library for event sourcing in Python.

*"totally amazing and a pleasure to use"*

[Read the docs](https://eventsourcing.readthedocs.io/). See also [extension projects](https://github.com/pyeventsourcing).

[![Foot](https://upload.wikimedia.org/wikipedia/commons/8/89/Foot_detail_from_Venus%2C_Cupid%2C_Folly_and_Time_by_Agnolo_Bronzino.jpg)](https://en.wikipedia.org/wiki/Monty_Python%27s_Flying_Circus)

## Installation

Use pip to install the [stable distribution](https://pypi.org/project/eventsourcing/)
from the Python Package Index. Please note, it is recommended to
install Python packages into a Python virtual environment.

    $ pip install eventsourcing

## Synopsis

Use the library's `Aggregate` base class and `@event` decorator to define
event-sourced aggregates.

Derive your aggregate classes from the `Aggregate`
base class. Create new aggregate instances by calling the derived
aggregate class. Use the `@event` decorator to define aggregate event
classes. Events will be triggered when decorated methods are called.

```python
from eventsourcing.domain import Aggregate, event

class World(Aggregate):
    @event('Created')
    def __init__(self, name: str) -> None:
        self.name = name
        self.history = ()

    @event('SomethingHappened')
    def make_it_so(self, what: str) -> None:
        self.history += (what,)
```

Use the library's `Application` class to define an event-sourced application.
Applications store and retrieve aggregates, using a persistence mechanism for
aggregate events.

Derive your application classes from the `Application` base class. Add command
and query methods to manipulate and access the state of the application.

```python
from typing import Tuple
from uuid import UUID

from eventsourcing.application import Application

class Universe(Application):
    def create_world(self, name: str) -> UUID:
        world = World(name)
        self.save(world)
        return world.id

    def make_it_so(self, world_id: UUID, what: str) -> None:
        world = self._get_world(world_id)
        world.make_it_so(what)
        self.save(world)

    def get_history(self, world_id) -> Tuple:
        return self._get_world(world_id).history

    def _get_world(self, world_id) -> World:
        world = self.repository.get(world_id)
        assert isinstance(world, World)
        return world

```

Construct an instance of the application by calling the application class.

```python
application = Universe()

```

Create a new aggregate by calling the application method `create_world()`.

```python
world_id = application.create_world('Earth')
```

Evolve the state of the application's aggregate by calling the
application command method `make_it_so()`.

```python
application.make_it_so(world_id, 'dinosaurs')
application.make_it_so(world_id, 'trucks')
application.make_it_so(world_id, 'internet')

```

Access the state of the application's aggregate by calling the
application query method `get_history()`.

```python
history = application.get_history(world_id)
assert history == ('dinosaurs', 'trucks', 'internet')
```

See the discussion below and the library's
[documentation](https://eventsourcing.readthedocs.io/)
for more information.

## Features

**Aggregates and applications** — base classes for event-sourced aggregates
and applications. Suggests how to structure an event-sourced application. All
classes are fully type-hinted to guide developers in using the library.

**Flexible event store** — flexible persistence of aggregate events. Combines
an event mapper and an event recorder in ways that can be easily extended.
Mapper uses a transcoder that can be easily extended to support custom
model object types. Recorders supporting different databases can be easily
substituted and configured with environment variables.

**Application-level encryption and compression** — encrypts and decrypts events inside the
application. This means data will be encrypted in transit across a network ("on the wire")
and at disk level including backups ("at rest"), which is a legal requirement in some
jurisdictions when dealing with personally identifiable information (PII) for example
the EU's GDPR. Compression reduces the size of stored aggregate events and snapshots, usually
by around 25% to 50% of the original size. Compression reduces the size of data
in the database and decreases transit time across a network.

**Snapshotting** — reduces access-time for aggregates that have many events.

**Versioning** - allows changes to be introduced after an application
has been deployed. Both aggregate events and aggregate snapshots can be versioned.

**Optimistic concurrency control** — ensures a distributed or horizontally scaled
application doesn't become inconsistent due to concurrent method execution. Leverages
optimistic concurrency controls in adapted database management systems.

**Notifications and projections** — reliable propagation of application
events with pull-based notifications allows the application state to be
projected accurately into replicas, indexes, view models, and other applications.
Supports materialized views and CQRS.

**Event-driven systems** — reliable event processing. Event-driven systems
can be defined independently of particular persistence infrastructure and mode of
running.

**Detailed documentation** — documentation provides general overview, introduction
of concepts, explanation of usage, and detailed descriptions of library classes.

**Worked examples** — includes examples showing how to develop aggregates, applications
and systems.

## Aggregates

The example above shows an event-sourced aggregate class named
`World`. Its ``__init__()`` method takes a `name` argument which
is used to initialise the `name` attribute, and also initialises
a `history` attribute as an empty tuple. It has a method `make_it_so()`
that takes an argument `what` and appends the given value to the
`history` of the `World`.

The `World` class uses the aggregate base class `Aggregate` from
the library's `domain` module. It uses the `@event` decorator to
define two aggregate event classes, `Created` and `SomethingHappened`.
The attributes of the event classes match the parameters of the
decorated method signature.

Calling the `World` aggregate class will construct a new aggregate instance.

```python
# Call the aggregate class to create a new aggregate object.
world = World(name='Earth')

# Check the name of the new aggregate object.
assert world.name == 'Earth'

# Check the state of the new aggregate object.
assert world.history == ()
```

The `World` aggregate object has an `id` attribute. It is a version 4
universally unique identifier (UUID). This follows from the default
behaviour of the `Aggregate` base class.

```python

# The aggregate has an ID.
assert isinstance(world.id, UUID)
```

Calling the `make_it_so()` command method will change the state of the aggregate.

```python
# Execute aggregate commands.
world.make_it_so('dinosaurs')
world.make_it_so('trucks')
world.make_it_so('internet')

# Check aggregate state.
assert world.history[0] == 'dinosaurs'
assert world.history[1] == 'trucks'
assert world.history[2] == 'internet'
```

How does it work? When the aggregate class is called, a `Created` event class
is constructed. This event object instance is used to construct an instance
of the `World` aggregate class. The `Created` event class is defined with an
attribute `name`. The event object instance will have the value of the `name`
argument given when calling the aggregate class. After the aggregate object
is instantiated, the event object is appended to an internal list of pending
events. The `World` object is then returned to the caller.

Similarly, when the command method `make_it_so()` is called on an instance of
the `World` class, a `SomethingHappened` event is constructed. The
`SomethingHappened` event object class is defined with an attribute `what`.
The event object instance will have the value of the `what` argument given
when calling the command method. This event object is used to evolve the
state of the aggregate object, using the body of the decorated method.
Hence, the value `what` is appended to the `history` of the `World`
aggregate instance. After the `SomethingHappened` event is applied to the
aggregate, the event will be immediately appended to the aggregate's internal
list of pending events.

Pending event objects can be collected using the aggregate's `collect_events()`
method. The aggregate event classes are defined on the aggregate class, so that
they are "namespaced" within the aggregate class. Hence, the "fully qualified"
name of the `Created` event is `World.Created`. And the "fully qualified" name
of the `SomethingHappened` event is `World.SomethingHappened`.

```python
# Collect events.
events = world.collect_events()
assert len(events) == 4

assert type(events[0]).__name__ == 'Created'
assert type(events[1]).__name__ == 'SomethingHappened'
assert type(events[2]).__name__ == 'SomethingHappened'
assert type(events[3]).__name__ == 'SomethingHappened'

assert type(events[0]).__qualname__ == 'World.Created'
assert type(events[1]).__qualname__ == 'World.SomethingHappened'
assert type(events[2]).__qualname__ == 'World.SomethingHappened'
assert type(events[3]).__qualname__ == 'World.SomethingHappened'
```

Collecting and storing aggregate events and reconstructing aggregates
from stored events are responsibilities of the library's `Application`
class. The `collect_events()` method is used by the application's
`save()` method to collect aggregate events, so the event objects can
be stored in a database. You probably won't need to call this method
explicitly in your project code.

The collected event objects can be used to reconstruct the state of
the aggregate. The application repository's `get()` method
reconstructs aggregates from stored events in this way (see below).

```python
copy = None
for event in events:
    copy = event.mutate(copy)

assert copy.history == world.history
assert copy.id == world.id
assert copy.version == world.version
assert copy.created_on == world.created_on
assert copy.modified_on == world.modified_on
```

Please note, the body of a method decorated with the `@event` decorator
will be executed each time the associated event is applied to evolve
the state of the aggregate, both just after triggering the event and
each time when reconstructing aggregates from stored events. For this
reason, the body of the decorated method should return the Python value
`None`. It is normal for command methods to return `None`. However, if
you do need to return values from your aggregate command methods, then
define a command method that is not decorated with the `@event` decorator,
and return the value from the non-decorated method after calling the decorated
method.

Any processing of method arguments that should be done only once, and not
repeated when reconstructing aggregates from stored events, should be done
in the calling method. For example, if the triggered event will have a new
UUID, you will either want to use a separate command method, or create this
value in the expression used when calling the method, and not generate the
UUID in the decorated method body. Otherwise, rather than being fixed in the
stored state of the aggregate, a new UUID will be created each time the aggregate
is reconstructed. See the library's
[domain module documentation](https://eventsourcing.readthedocs.io/)
for more information.

Please also note, if an exception happens to be raised in the decorated method
body, then the triggered event will not be appended to the internal list of
pending events as described above. If you are careful, this behaviour (of not
appending the event to the list of pending events) can be used to validate
the state of the event against the current state of the aggregate. But if
you wish to continue using the same aggregate instance after catching a
validation exception in the caller of the decorated method, please just be
careful to complete all validation before adjusting the state of the aggregate,
otherwise you will need to retrieve a fresh instance from the repository.

## Applications

An "application" object in this library roughly corresponds to a "bounded context"
in Domain-Driven Design. An application can have aggregates of different types in
its domain model.

The example above defines an event-sourced application named
`Universe`. The application class `Universe` uses the
application base class `Application` from the library's
`application` module. When the `Universe` application class is
called, an application object is constructed.

A `Universe` application object has a command
method `create_world()` that creates and saves new instances of
the aggregate class `World`. It has a command method
`make_it_so()` that calls the aggregate command method
`make_it_so()` of an already existing aggregate object. And it
has a query method `get_history()` that returns the `history` of
an aggregate object.

When the application command method `create_world()` is called,
a new `World` aggregate object is created, the new aggregate
object is saved by calling the application's `save()` method,
and then the ID of the aggregate is returned to the caller.

When the application command method `make_it_so()` is called with
the ID of an aggregate, the repository is used to get the
aggregate, the aggregate's `make_it_so()` method is called with
the given value of `what`, and the aggregate is saved by calling
the application's `save()` method.

When the application query method `get_history()` is called with
the ID of an aggregate, the repository is used to get the
aggregate, and the value of the aggregate's `history` attribute
is returned to the caller.

How does it work? The `Application` class provides persistence
infrastructure that can collect, serialise, and store aggregate
events. It can also reconstruct aggregates from stored events.

The application `save()` method saves aggregates by
collecting and storing pending aggregate events. The `save()`
method calls the given aggregate's `collect_events()` method and
puts the pending aggregate events in an event store, with a
guarantee that either all the events will be stored or none of
them will be.

The application `repository` has a `get()`
method that can be used to obtain previously saved aggregates.
The `get()` method is called with an aggregate ID. It retrieves
stored events for an aggregate from an event store, then
reconstructs the aggregate object from its previously stored
events (see above), and then returns the reconstructed aggregate object to
the caller. The application class can be configured using
environment variables to work with different databases, and
optionally to encrypt and compress stored events. By default,
the application serialises aggregate events using JSON, and
stores them in memory as "plain old Python objects". The library
includes support for storing events in SQLite and PosgreSQL (see
below). Other databases are available.

The `Application` class also has a `log` object which can be
used to get all the aggregate events that have been stored
across all the aggregates of an application. The log presents
the aggregate events in the order in which they were stored,
as a sequence of event notifications. Each of the event
notifications has an integer ID which increases along the
sequence. The `log` can be used to propagate the state of
the application in a manner that supports deterministic
processing of the application state in event-driven systems.

```python
log_section = application.log['1,4']
notifications = log_section.items
assert [n.id for n in notifications] == [1, 2, 3, 4]

assert 'World.Created' in notifications[0].topic
assert 'World.SomethingHappened' in notifications[1].topic
assert 'World.SomethingHappened' in notifications[2].topic
assert 'World.SomethingHappened' in notifications[3].topic

assert b'Earth' in notifications[0].state
assert b'dinosaurs' in notifications[1].state
assert b'trucks' in notifications[2].state
assert b'internet' in notifications[3].state

assert world_id == notifications[0].originator_id
assert world_id == notifications[1].originator_id
assert world_id == notifications[2].originator_id
assert world_id == notifications[3].originator_id
```

The `test()` function below demonstrates the example in more detail,
by creating many aggregates in one application, reading event
notifications from the application log, retrieving historical
versions of an aggregate. The optimistic concurrency control
feature, and the compression and encryption features are also
demonstrated.

```python
from eventsourcing.persistence import IntegrityError
from eventsourcing.system import NotificationLogReader

def test(app: Universe, expect_visible_in_db: bool):
    # Check app has zero event notifications.
    assert len(app.log['1,10'].items) == 0

    # Create a new aggregate.
    world_id = app.create_world('Earth')

    # Execute application commands.
    app.make_it_so(world_id, 'dinosaurs')
    app.make_it_so(world_id, 'trucks')

    # Check recorded state of the aggregate.
    assert app.get_history(world_id) == (
        'dinosaurs',
        'trucks'
    )

    # Execute another command.
    app.make_it_so(world_id, 'internet')

    # Check recorded state of the aggregate.
    assert app.get_history(world_id) == (
        'dinosaurs',
        'trucks',
        'internet'
    )

    # Check values are (or aren't visible) in the database.
    values = [b'dinosaurs', b'trucks', b'internet']
    if expect_visible_in_db:
        expected_num_visible = len(values)
    else:
        expected_num_visible = 0

    actual_num_visible = 0
    reader = NotificationLogReader(app.log)
    for notification in reader.read(start=1):
        for what in values:
            if what in notification.state:
                actual_num_visible += 1
                break
    assert expected_num_visible == actual_num_visible

    # Get historical state (at version 3, before 'internet' happened).
    old = app.repository.get(world_id, version=3)
    assert len(old.history) == 2
    assert old.history[-1] == 'trucks'  # last thing to have happened was 'trucks'

    # Check app has four event notifications.
    assert len(app.log['1,10'].items) == 4

    # Optimistic concurrency control (no branches).
    old.make_it_so('future')
    try:
        app.save(old)
    except IntegrityError:
        pass
    else:
        raise Exception("Shouldn't get here")

    # Check app still has only four event notifications.
    assert len(app.log['1,10'].items) == 4

    # Read event notifications.
    reader = NotificationLogReader(app.log)
    notifications = list(reader.read(start=1))
    assert len(notifications) == 4

    # Create eight more aggregate events.
    world_id = app.create_world('Mars')
    app.make_it_so(world_id, 'plants')
    app.make_it_so(world_id, 'fish')
    app.make_it_so(world_id, 'mammals')

    world_id = app.create_world('Venus')
    app.make_it_so(world_id, 'morning')
    app.make_it_so(world_id, 'afternoon')
    app.make_it_so(world_id, 'evening')

    # Get the new event notifications from the reader.
    last_id = notifications[-1].id
    notifications = list(reader.read(start=last_id + 1))
    assert len(notifications) == 8

    # Get all the event notifications from the application log.
    notifications = list(reader.read(start=1))
    assert len(notifications) == 12
```

This example can be adjusted and extended for any event-sourced application.

## Project structure

You are free to structure your project files however you wish. You
may wish to put your aggregate classes in a file named
`domainmodel.py` and your application class in a file named
`application.py`.

    myproject/
    myproject/application.py
    myproject/domainmodel.py
    myproject/tests.py

But you can start by first writing a failing test in `tests.py`, then define
your application and aggregate classes in the test module, and then refactor
by moving things to separate Python modules.

## Development environment

We can run the code in default "development" environment using
the default "Plain Old Python Object" infrastructure (which keeps
stored events in memory). The example below runs with no compression or
encryption of the stored events.

```python
# Construct an application object.
app = Universe()

# Run the test.
test(app, expect_visible_in_db=True)

```

## SQLite environment

You can configure "production" environment to use the library's
SQLite infrastructure with the following environment variables.
Using SQLite infrastructure will keep stored events in an
SQLite database.

An in-memory SQLite database is used in this example. To store your events on
disk, use a file path as the value of the `SQLITE_DBNAME` environment variable.

```python
import os

# Use SQLite infrastructure.
os.environ['INFRASTRUCTURE_FACTORY'] = 'eventsourcing.sqlite:Factory'

# Configure SQLite database URI. Either use a file-based DB;
os.environ['SQLITE_DBNAME'] = '/path/to/your/sqlite-db'
# or use an in-memory DB with cache not shared, only works with single thread;
os.environ['SQLITE_DBNAME'] = ':memory:'
# or use an in-memory DB with shared cache, works with multiple threads;
os.environ['SQLITE_DBNAME'] = ':memory:?mode=memory&cache=shared'
# or use a named in-memory DB, allows distinct databases in same process.
os.environ['SQLITE_DBNAME'] = 'file:application1?mode=memory&cache=shared'

# Set optional lock timeout (default 5s).
os.environ['SQLITE_LOCK_TIMEOUT'] = '10'  # seconds
```

Please note, a file-based SQLite database will have its journal mode set to use
write-ahead logging (WAL), which allows reading to proceed concurrently reading
and writing. Writing is serialised with a lock. The lock timeout can be adjusted
from the SQLite default of 5s by setting the environment variable `SQLITE_LOCK_TIMEOUT`.

Optionally, set the cipher key using environment variable `CIPHER_KEY` and select a
compressor by setting environment variable `COMPRESSOR_TOPIC`.

This example uses the Python `zlib` module to compress stored events, and AES
to encrypt the compressed stored events, before writing them to the SQLite database.
To use the library's encryption functionality, please install the library with the
`crypto` option (or just install the `pycryptodome` package.) To use an alternative
cipher strategy, set the environment variable `CIPHER_TOPIC`.

    $ pip install eventsourcing[crypto]

```python
from eventsourcing.cipher import AESCipher

# Generate a cipher key (keep this safe).
cipher_key = AESCipher.create_key(num_bytes=32)

# Cipher key.
os.environ['CIPHER_KEY'] = cipher_key

# Compressor topic.
os.environ['COMPRESSOR_TOPIC'] = 'zlib'
```

Having configured the application with these environment variables, we
can construct the application and run the test using SQLite.

```python
# Construct an application object.
app = Universe()

# Run the test.
test(app, expect_visible_in_db=False)
```

## PostgreSQL environment

You can configure "production" environment to use the library's
PostgresSQL infrastructure with the following environment variables.
Using PostgresSQL infrastructure will keep stored events in a
PostgresSQL database.

Please note, to use the library's PostgreSQL functionality,
please install the library with the `postgres` option (or just
install the `psycopg2` package.)

    $ pip install eventsourcing[postgres]

Please note, the library option `postgres_dev` will install the
`psycopg2-binary` which is much faster, but this is not recommended
for production use. The binary package is a practical choice for
development and testing but in production it is advised to use
the package built from sources.

The example below also uses zlib and AES to compress and encrypt the
stored events (but this is optional). To use the library's
encryption functionality with PostgreSQL, please install the library
with both the `crypto` and the `postgres` option (or just install the
`pycryptodome` and `psycopg2` packages.)

    $ pip install eventsourcing[crypto,postgres]

It is assumed for this example that the database and database user have
already been created, and the database server is running locally.

```python
import os

from eventsourcing.cipher import AESCipher

# Generate a cipher key (keep this safe).
cipher_key = AESCipher.create_key(num_bytes=32)

# Cipher key.
os.environ['CIPHER_KEY'] = cipher_key
# Cipher topic.
os.environ['CIPHER_TOPIC'] = 'eventsourcing.cipher:AESCipher'
# Compressor topic.
os.environ['COMPRESSOR_TOPIC'] = 'eventsourcing.compressor:ZlibCompressor'

# Use Postgres infrastructure.
os.environ['INFRASTRUCTURE_FACTORY'] = 'eventsourcing.postgres:Factory'

# Configure database connections.
os.environ['POSTGRES_DBNAME'] = 'eventsourcing'
os.environ['POSTGRES_HOST'] = '127.0.0.1'
os.environ['POSTGRES_PORT'] = '5432'
os.environ['POSTGRES_USER'] = 'eventsourcing'
os.environ['POSTGRES_PASSWORD'] = 'eventsourcing'

# Optional config.
# - connection max age (connections stay open by default)
os.environ['POSTGRES_CONN_MAX_AGE'] = '60'  # seconds
# - check connection before use (pessimistic disconnect handling, default 'n')
os.environ['POSTGRES_PRE_PING'] = 'y'
# - timeout to wait for table lock when inserting (default no timeout)
os.environ['POSTGRES_LOCK_TIMEOUT'] = '10'  # seconds
# - timeout for sessions with idle transactions (default no timeout)
os.environ['POSTGRES_IDLE_IN_TRANSACTION_SESSION_TIMEOUT'] = '10'  # seconds
```

Please note, to avoid interleaving of inserts when writing events, an
'EXCLUSIVE' mode table lock is acquired when using PostgreSQL. This
effectively serialises writing events. It prevents concurrent transactions
interleaving inserts, which would potentially cause notification log readers
that are tailing the application notification log to miss event notifications.
Reading from the table can proceed concurrently with other readers and writers,
since selecting acquires an 'ACCESS SHARE' lock which does not block and
is not blocked by the 'EXCLUSIVE' lock. This issue of interleaving inserts
by concurrent writers is not exhibited by SQLite, which supports concurrent
readers when its journal mode is set to use write ahead logging.

Having configured the application with these environment variables,
we can construct the application and run the test using PostgreSQL.

```python
# Construct an application object.
app = Universe()

# Run the test.
test(app, expect_visible_in_db=False)
```

## Extensions

The GitHub organisation
[Event Sourcing in Python](https://github.com/pyeventsourcing)
hosts extension projects for the Python eventsourcing library.
There are projects that support ORMs such as [Django](https://github.com/pyeventsourcing/eventsourcing-django) and [SQLAlchemy](https://github.com/pyeventsourcing/eventsourcing-sqlalchemy).
There are projects supporting databases such as AxonDB, DynamoDB,
EventStoreDB, and Apache Kafka. Another project supports
transcoding domain events with Protocol Buffers rather than JSON.
There are also projects that provide examples of using the
library with such things as [FastAPI](https://github.com/pyeventsourcing/example-fastapi),
Flask, and serverless.

## Project

This project is [hosted on GitHub](https://github.com/johnbywater/eventsourcing).

Please register questions, requests and
[issues on GitHub](https://github.com/johnbywater/eventsourcing/issues),
or post in the project's Slack channel.

There is a [Slack channel](https://join.slack.com/t/eventsourcinginpython/shared_invite/enQtMjczNTc2MzcxNDI0LTJjMmJjYTc3ODQ3M2YwOTMwMDJlODJkMjk3ZmE1MGYyZDM4MjIxODZmYmVkZmJkODRhZDg5N2MwZjk1YzU3NmY)
for this project, which you are [welcome to join](https://join.slack.com/t/eventsourcinginpython/shared_invite/enQtMjczNTc2MzcxNDI0LTJjMmJjYTc3ODQ3M2YwOTMwMDJlODJkMjk3ZmE1MGYyZDM4MjIxODZmYmVkZmJkODRhZDg5N2MwZjk1YzU3NmY).

Please refer to the [documentation](https://eventsourcing.readthedocs.io/) for installation and usage guides.