pgoutput-client 0.0.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +37 -2
- data/LICENSE.txt +6 -6
- data/README.md +346 -19
- data/lib/pgoutput/client/commands.rb +62 -0
- data/lib/pgoutput/client/configuration.rb +187 -0
- data/lib/pgoutput/client/connection.rb +120 -0
- data/lib/pgoutput/client/errors.rb +41 -0
- data/lib/pgoutput/client/feedback.rb +62 -0
- data/lib/pgoutput/client/keepalive.rb +64 -0
- data/lib/pgoutput/client/lsn.rb +54 -0
- data/lib/pgoutput/client/runner.rb +258 -0
- data/lib/pgoutput/client/state.rb +41 -0
- data/lib/pgoutput/client/stream.rb +163 -0
- data/lib/pgoutput/client/version.rb +4 -1
- data/lib/pgoutput/client/xlog_data.rb +71 -0
- data/lib/pgoutput/client.rb +23 -2
- data/lib/pgoutput_client.rb +12 -0
- data/sig/pg.rbs +12 -0
- data/sig/pgoutput/client/commands.rbs +42 -0
- data/sig/pgoutput/client/configuration.rbs +436 -0
- data/sig/pgoutput/client/connection.rbs +91 -0
- data/sig/pgoutput/client/errors.rbs +43 -0
- data/sig/pgoutput/client/feedback.rbs +71 -0
- data/sig/pgoutput/client/keepalive.rbs +55 -0
- data/sig/pgoutput/client/lsn.rbs +36 -0
- data/sig/pgoutput/client/runner.rbs +99 -0
- data/sig/pgoutput/client/state.rbs +29 -0
- data/sig/pgoutput/client/stream.rbs +89 -0
- data/sig/pgoutput/client/version.rbs +8 -0
- data/sig/pgoutput/client/xlog_data.rbs +63 -0
- metadata +50 -11
- data/CODE_OF_CONDUCT.md +0 -10
- data/Rakefile +0 -12
- data/sig/pgoutput/client.rbs +0 -6
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
module Pgoutput
|
|
2
|
+
module Client
|
|
3
|
+
# PostgreSQL Log Sequence Number conversion helpers.
|
|
4
|
+
#
|
|
5
|
+
# PostgreSQL represents LSNs as two hexadecimal 32-bit halves separated by a
|
|
6
|
+
# slash, such as `0/16B6C50`. The replication protocol transmits the same WAL
|
|
7
|
+
# position as an unsigned 64-bit integer. This module converts between those
|
|
8
|
+
# two representations.
|
|
9
|
+
#
|
|
10
|
+
# @example Parse a textual LSN
|
|
11
|
+
# Pgoutput::Client::LSN.parse("0/10")
|
|
12
|
+
# # => 16
|
|
13
|
+
#
|
|
14
|
+
# @example Format an integer WAL position
|
|
15
|
+
# Pgoutput::Client::LSN.format(16)
|
|
16
|
+
# # => "0/10"
|
|
17
|
+
#
|
|
18
|
+
# @api public
|
|
19
|
+
module LSN
|
|
20
|
+
# Parse a PostgreSQL LSN string into an integer WAL position.
|
|
21
|
+
#
|
|
22
|
+
# @param value [#to_s] LSN string in `HEX/HEX` form
|
|
23
|
+
# @return [Integer] unsigned 64-bit WAL position
|
|
24
|
+
# @raise [ArgumentError] if the value is not a valid LSN string
|
|
25
|
+
def self?.parse: (untyped value) -> untyped
|
|
26
|
+
|
|
27
|
+
# Format an integer WAL position as a PostgreSQL LSN string.
|
|
28
|
+
#
|
|
29
|
+
# @param value [#to_int, #to_s] non-negative integer WAL position
|
|
30
|
+
# @return [String] LSN string in uppercase hexadecimal `HEX/HEX` form
|
|
31
|
+
# @raise [ArgumentError] if the value is negative or cannot be coerced to
|
|
32
|
+
# an integer
|
|
33
|
+
def self?.format: (untyped value) -> ::String
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
module Pgoutput
|
|
2
|
+
module Client
|
|
3
|
+
# High-level logical replication client facade.
|
|
4
|
+
#
|
|
5
|
+
# `Runner` is the simplest public entry point for consumers that want to
|
|
6
|
+
# stream raw pgoutput payloads without manually managing a replication
|
|
7
|
+
# connection. It owns the connection lifecycle for one logical replication
|
|
8
|
+
# stream:
|
|
9
|
+
#
|
|
10
|
+
# 1. Build an immutable {Configuration} from keyword arguments.
|
|
11
|
+
# 2. Open a PostgreSQL replication connection.
|
|
12
|
+
# 3. Optionally create the configured replication slot.
|
|
13
|
+
# 4. Start logical replication.
|
|
14
|
+
# 5. Yield raw pgoutput payload bytes and {XLogData} metadata.
|
|
15
|
+
# 6. Close the connection when streaming exits.
|
|
16
|
+
#
|
|
17
|
+
# If a live stream loses its connection, the runner retries a small number
|
|
18
|
+
# of times with a backoff and resumes from the latest confirmed WAL
|
|
19
|
+
# position. Replay, checkpointing, and deduplication are not owned here;
|
|
20
|
+
# those concerns belong to the downstream CDC runtime and sink layer.
|
|
21
|
+
#
|
|
22
|
+
# @example Stream raw pgoutput messages
|
|
23
|
+
# runner = Pgoutput::Client::Runner.new(
|
|
24
|
+
# database_url: "postgres://localhost/app",
|
|
25
|
+
# slot_name: "cdc_slot",
|
|
26
|
+
# publication_names: ["app_publication"]
|
|
27
|
+
# )
|
|
28
|
+
#
|
|
29
|
+
# runner.start do |payload, metadata|
|
|
30
|
+
# puts "received #{payload.bytesize} bytes at #{metadata.wal_end_lsn}"
|
|
31
|
+
# end
|
|
32
|
+
#
|
|
33
|
+
# @see Configuration
|
|
34
|
+
# @see Connection
|
|
35
|
+
# @see Stream
|
|
36
|
+
# @api public
|
|
37
|
+
class Runner
|
|
38
|
+
@configuration: untyped
|
|
39
|
+
|
|
40
|
+
@stopped: untyped
|
|
41
|
+
|
|
42
|
+
@running: untyped
|
|
43
|
+
|
|
44
|
+
@stream: untyped
|
|
45
|
+
|
|
46
|
+
@resume_lsn: untyped
|
|
47
|
+
|
|
48
|
+
@acked_lsn: untyped
|
|
49
|
+
|
|
50
|
+
@slot_created: untyped
|
|
51
|
+
|
|
52
|
+
@last_error: untyped
|
|
53
|
+
|
|
54
|
+
@reconnect_attempts: untyped
|
|
55
|
+
|
|
56
|
+
DEFAULT_RECONNECT_ATTEMPTS: 3
|
|
57
|
+
|
|
58
|
+
DEFAULT_RECONNECT_BACKOFF: ::Float
|
|
59
|
+
|
|
60
|
+
attr_reader configuration: untyped
|
|
61
|
+
|
|
62
|
+
attr_reader last_error: untyped
|
|
63
|
+
|
|
64
|
+
def initialize: (**untyped options) -> void
|
|
65
|
+
|
|
66
|
+
def start: () ?{ (?) -> untyped } -> untyped
|
|
67
|
+
|
|
68
|
+
def stop: () -> nil
|
|
69
|
+
|
|
70
|
+
def restart: () ?{ (?) -> untyped } -> untyped
|
|
71
|
+
|
|
72
|
+
def running?: () -> bool
|
|
73
|
+
|
|
74
|
+
def stopped?: () -> bool
|
|
75
|
+
|
|
76
|
+
def connected?: () -> bool
|
|
77
|
+
|
|
78
|
+
def ack: (untyped lsn) -> untyped
|
|
79
|
+
|
|
80
|
+
def monitor: () -> Pgoutput::Client::RunnerState
|
|
81
|
+
|
|
82
|
+
private
|
|
83
|
+
|
|
84
|
+
def setup_connection: (untyped connection) -> untyped
|
|
85
|
+
|
|
86
|
+
def run_stream_cycle: (untyped configuration) { (?) -> untyped } -> untyped
|
|
87
|
+
|
|
88
|
+
def configuration_for_resume: () -> untyped
|
|
89
|
+
|
|
90
|
+
def normalize_lsn_value: (untyped value) -> untyped
|
|
91
|
+
|
|
92
|
+
def current_lsn_string: (untyped value) -> untyped
|
|
93
|
+
|
|
94
|
+
def reconnect_backoff_for: (untyped attempts) -> untyped
|
|
95
|
+
|
|
96
|
+
def sleep: (untyped duration) -> untyped
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
module Pgoutput
|
|
2
|
+
module Client
|
|
3
|
+
class RunnerStateData < Data
|
|
4
|
+
attr_reader running: bool
|
|
5
|
+
attr_reader stop_requested: bool
|
|
6
|
+
attr_reader connected: bool
|
|
7
|
+
attr_reader last_received_lsn: String?
|
|
8
|
+
attr_reader last_feedback_lsn: String?
|
|
9
|
+
attr_reader last_keepalive_at: Time?
|
|
10
|
+
attr_reader last_error: String?
|
|
11
|
+
attr_reader reconnect_attempts: Integer
|
|
12
|
+
|
|
13
|
+
def self.new: (
|
|
14
|
+
running: bool,
|
|
15
|
+
stop_requested: bool,
|
|
16
|
+
connected: bool,
|
|
17
|
+
last_received_lsn: String?,
|
|
18
|
+
last_feedback_lsn: String?,
|
|
19
|
+
last_keepalive_at: Time?,
|
|
20
|
+
last_error: String?,
|
|
21
|
+
reconnect_attempts: Integer
|
|
22
|
+
) -> instance
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
class RunnerState < RunnerStateData
|
|
26
|
+
def stopped?: () -> bool
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
module Pgoutput
|
|
2
|
+
module Client
|
|
3
|
+
# Logical replication stream loop.
|
|
4
|
+
#
|
|
5
|
+
# `Stream` consumes PostgreSQL CopyData payloads from a {Connection}. It
|
|
6
|
+
# understands the two replication envelope message types used by PostgreSQL's
|
|
7
|
+
# streaming replication protocol:
|
|
8
|
+
#
|
|
9
|
+
# * `w` — XLogData, containing logical decoding plugin payload bytes.
|
|
10
|
+
# * `k` — primary keepalive, optionally requesting immediate feedback.
|
|
11
|
+
#
|
|
12
|
+
# The stream yields only XLogData plugin payloads. Keepalive messages are
|
|
13
|
+
# handled internally by updating the latest known WAL position and sending
|
|
14
|
+
# standby feedback when requested.
|
|
15
|
+
#
|
|
16
|
+
# @api private
|
|
17
|
+
class Stream
|
|
18
|
+
@connection: untyped
|
|
19
|
+
|
|
20
|
+
@configuration: untyped
|
|
21
|
+
|
|
22
|
+
@latest_lsn: untyped
|
|
23
|
+
|
|
24
|
+
@acked_lsn: untyped
|
|
25
|
+
|
|
26
|
+
@last_feedback_at: untyped
|
|
27
|
+
|
|
28
|
+
@last_keepalive_at: untyped
|
|
29
|
+
|
|
30
|
+
@running: untyped
|
|
31
|
+
|
|
32
|
+
@stop_requested: untyped
|
|
33
|
+
|
|
34
|
+
# Latest confirmed WAL position observed by this stream.
|
|
35
|
+
#
|
|
36
|
+
# @return [Integer]
|
|
37
|
+
attr_reader latest_lsn: untyped
|
|
38
|
+
|
|
39
|
+
attr_reader acked_lsn: untyped
|
|
40
|
+
|
|
41
|
+
attr_reader last_keepalive_at: untyped
|
|
42
|
+
|
|
43
|
+
# Build a stream loop.
|
|
44
|
+
#
|
|
45
|
+
# @param connection [Connection] replication connection
|
|
46
|
+
# @param configuration [Configuration] stream configuration
|
|
47
|
+
# @return [void]
|
|
48
|
+
def initialize: (connection: untyped, configuration: untyped, ?acked_lsn: untyped) -> void
|
|
49
|
+
|
|
50
|
+
# Start the stream loop.
|
|
51
|
+
#
|
|
52
|
+
# The method blocks while the stream is running. For every XLogData
|
|
53
|
+
# envelope, it yields the raw pgoutput payload and the parsed envelope
|
|
54
|
+
# metadata. When no CopyData payload is currently available, the loop
|
|
55
|
+
# pauses briefly before checking again.
|
|
56
|
+
#
|
|
57
|
+
# @yield [payload, metadata] called for each XLogData payload
|
|
58
|
+
# @yieldparam payload [String] frozen raw pgoutput payload bytes
|
|
59
|
+
# @yieldparam metadata [XLogData] parsed WAL envelope metadata
|
|
60
|
+
# @return [void]
|
|
61
|
+
# @raise [ArgumentError] if no block is provided
|
|
62
|
+
# @raise [ProtocolError] if an unknown or malformed replication message is
|
|
63
|
+
# received
|
|
64
|
+
# @raise [ConnectionError] if standby feedback cannot be sent
|
|
65
|
+
def start: () ?{ (?) -> untyped } -> untyped
|
|
66
|
+
|
|
67
|
+
# Stop the stream loop after the current iteration.
|
|
68
|
+
#
|
|
69
|
+
# @return [void]
|
|
70
|
+
def stop: () -> nil
|
|
71
|
+
|
|
72
|
+
def running?: () -> bool
|
|
73
|
+
|
|
74
|
+
def ack: (untyped lsn) -> untyped
|
|
75
|
+
|
|
76
|
+
private
|
|
77
|
+
|
|
78
|
+
def process_copy_data: (untyped copy_data) { (untyped, untyped) -> untyped } -> untyped
|
|
79
|
+
|
|
80
|
+
def send_periodic_feedback: () -> (nil | untyped)
|
|
81
|
+
|
|
82
|
+
def send_feedback: (reply_requested: untyped) -> untyped
|
|
83
|
+
|
|
84
|
+
def normalize_lsn_value: (untyped value) -> untyped
|
|
85
|
+
|
|
86
|
+
def monotonic_time: () -> untyped
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
module Pgoutput
|
|
2
|
+
module Client
|
|
3
|
+
class ReplicationXLogData < Data
|
|
4
|
+
attr_reader wal_start: Integer
|
|
5
|
+
attr_reader wal_end: Integer
|
|
6
|
+
attr_reader server_clock: Integer
|
|
7
|
+
attr_reader payload: String
|
|
8
|
+
|
|
9
|
+
def self.new: (
|
|
10
|
+
Integer wal_start,
|
|
11
|
+
Integer wal_end,
|
|
12
|
+
Integer server_clock,
|
|
13
|
+
String payload
|
|
14
|
+
) -> instance
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Immutable XLogData replication envelope.
|
|
18
|
+
#
|
|
19
|
+
# PostgreSQL wraps logical decoding plugin output in an XLogData CopyData
|
|
20
|
+
# payload while streaming replication is active. The payload layout is:
|
|
21
|
+
#
|
|
22
|
+
# ```text
|
|
23
|
+
# Byte 0 : message tag `w`
|
|
24
|
+
# Bytes 1-8 : WAL start position, unsigned 64-bit big-endian
|
|
25
|
+
# Bytes 9-16 : WAL end position, unsigned 64-bit big-endian
|
|
26
|
+
# Bytes 17-24 : server clock, PostgreSQL timestamp format
|
|
27
|
+
# Bytes 25.. : logical decoding plugin payload
|
|
28
|
+
# ```
|
|
29
|
+
#
|
|
30
|
+
# Instances are created through {.parse} and made shareable so they can cross
|
|
31
|
+
# Ractor boundaries with their frozen payload bytes.
|
|
32
|
+
#
|
|
33
|
+
# @attr_reader wal_start [Integer] WAL position where this message begins
|
|
34
|
+
# @attr_reader wal_end [Integer] WAL position after this message
|
|
35
|
+
# @attr_reader server_clock [Integer] PostgreSQL server timestamp in
|
|
36
|
+
# microseconds since 2000-01-01 UTC
|
|
37
|
+
# @attr_reader payload [String] frozen raw logical decoding plugin payload
|
|
38
|
+
class XLogData < ReplicationXLogData
|
|
39
|
+
# Parse an XLogData CopyData payload.
|
|
40
|
+
#
|
|
41
|
+
# @param bytes [String] raw CopyData payload beginning with `w`
|
|
42
|
+
# @return [XLogData] immutable parsed envelope
|
|
43
|
+
# @raise [ProtocolError] if the payload is empty, has the wrong message
|
|
44
|
+
# tag, or is too short to contain the required fields
|
|
45
|
+
def self.parse: (untyped bytes) -> untyped
|
|
46
|
+
|
|
47
|
+
# Starting WAL position formatted as a PostgreSQL LSN string.
|
|
48
|
+
#
|
|
49
|
+
# @return [String]
|
|
50
|
+
def wal_start_lsn: () -> untyped
|
|
51
|
+
|
|
52
|
+
# Ending WAL position formatted as a PostgreSQL LSN string.
|
|
53
|
+
#
|
|
54
|
+
# @return [String]
|
|
55
|
+
def wal_end_lsn: () -> untyped
|
|
56
|
+
|
|
57
|
+
# @param binary [String]
|
|
58
|
+
# @param offset [Integer]
|
|
59
|
+
# @return [Integer]
|
|
60
|
+
def self.unpack_u64: (untyped binary, untyped offset) -> untyped
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
metadata
CHANGED
|
@@ -1,15 +1,30 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: pgoutput-client
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ken C. Demanawa
|
|
8
|
-
bindir:
|
|
8
|
+
bindir: bin
|
|
9
9
|
cert_chain: []
|
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
-
dependencies:
|
|
12
|
-
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: pg
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '1.6'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '1.6'
|
|
26
|
+
description: Transport-only PostgreSQL logical replication client for receiving pgoutput
|
|
27
|
+
CopyData payloads.
|
|
13
28
|
email:
|
|
14
29
|
- kenneth.c.demanawa@gmail.com
|
|
15
30
|
executables: []
|
|
@@ -17,20 +32,44 @@ extensions: []
|
|
|
17
32
|
extra_rdoc_files: []
|
|
18
33
|
files:
|
|
19
34
|
- CHANGELOG.md
|
|
20
|
-
- CODE_OF_CONDUCT.md
|
|
21
35
|
- LICENSE.txt
|
|
22
36
|
- README.md
|
|
23
|
-
- Rakefile
|
|
24
37
|
- lib/pgoutput/client.rb
|
|
38
|
+
- lib/pgoutput/client/commands.rb
|
|
39
|
+
- lib/pgoutput/client/configuration.rb
|
|
40
|
+
- lib/pgoutput/client/connection.rb
|
|
41
|
+
- lib/pgoutput/client/errors.rb
|
|
42
|
+
- lib/pgoutput/client/feedback.rb
|
|
43
|
+
- lib/pgoutput/client/keepalive.rb
|
|
44
|
+
- lib/pgoutput/client/lsn.rb
|
|
45
|
+
- lib/pgoutput/client/runner.rb
|
|
46
|
+
- lib/pgoutput/client/state.rb
|
|
47
|
+
- lib/pgoutput/client/stream.rb
|
|
25
48
|
- lib/pgoutput/client/version.rb
|
|
26
|
-
-
|
|
27
|
-
|
|
49
|
+
- lib/pgoutput/client/xlog_data.rb
|
|
50
|
+
- lib/pgoutput_client.rb
|
|
51
|
+
- sig/pg.rbs
|
|
52
|
+
- sig/pgoutput/client/commands.rbs
|
|
53
|
+
- sig/pgoutput/client/configuration.rbs
|
|
54
|
+
- sig/pgoutput/client/connection.rbs
|
|
55
|
+
- sig/pgoutput/client/errors.rbs
|
|
56
|
+
- sig/pgoutput/client/feedback.rbs
|
|
57
|
+
- sig/pgoutput/client/keepalive.rbs
|
|
58
|
+
- sig/pgoutput/client/lsn.rbs
|
|
59
|
+
- sig/pgoutput/client/runner.rbs
|
|
60
|
+
- sig/pgoutput/client/state.rbs
|
|
61
|
+
- sig/pgoutput/client/stream.rbs
|
|
62
|
+
- sig/pgoutput/client/version.rbs
|
|
63
|
+
- sig/pgoutput/client/xlog_data.rbs
|
|
64
|
+
homepage: https://kanutocd.github.io/pgoutput-client/
|
|
28
65
|
licenses:
|
|
29
66
|
- MIT
|
|
30
67
|
metadata:
|
|
31
|
-
homepage_uri: https://github.
|
|
68
|
+
homepage_uri: https://kanutocd.github.io/pgoutput-client/
|
|
32
69
|
source_code_uri: https://github.com/kanutocd/pgoutput-client
|
|
33
70
|
changelog_uri: https://github.com/kanutocd/pgoutput-client/blob/main/CHANGELOG.md
|
|
71
|
+
documentation_uri: https://kanutocd.github.io/pgoutput-client/
|
|
72
|
+
rubygems_mfa_required: 'true'
|
|
34
73
|
rdoc_options: []
|
|
35
74
|
require_paths:
|
|
36
75
|
- lib
|
|
@@ -45,7 +84,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
45
84
|
- !ruby/object:Gem::Version
|
|
46
85
|
version: '0'
|
|
47
86
|
requirements: []
|
|
48
|
-
rubygems_version:
|
|
87
|
+
rubygems_version: 3.6.9
|
|
49
88
|
specification_version: 4
|
|
50
|
-
summary:
|
|
89
|
+
summary: PostgreSQL pgoutput logical replication transport client.
|
|
51
90
|
test_files: []
|
data/CODE_OF_CONDUCT.md
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
# Code of Conduct
|
|
2
|
-
|
|
3
|
-
"pgoutput-client" follows [The Ruby Community Conduct Guideline](https://www.ruby-lang.org/en/conduct) in all "collaborative space", which is defined as community communications channels (such as mailing lists, submitted patches, commit comments, etc.):
|
|
4
|
-
|
|
5
|
-
* Participants will be tolerant of opposing views.
|
|
6
|
-
* Participants must ensure that their language and actions are free of personal attacks and disparaging personal remarks.
|
|
7
|
-
* When interpreting the words and actions of others, participants should always assume good intentions.
|
|
8
|
-
* Behaviour which can be reasonably considered harassment will not be tolerated.
|
|
9
|
-
|
|
10
|
-
If you have any concerns about behaviour within this project, please contact us at ["kenneth.c.demanawa@gmail.com"](mailto:"kenneth.c.demanawa@gmail.com").
|
data/Rakefile
DELETED