cuzk-rest 0.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: f41325469105b4cb0016acf55195d3b3de369bf82a0e5584eb645ef0476330ee
4
+ data.tar.gz: d9acd5e9d4e8d1a53ca779165e8b1941fadd1d5553eb50ab53327af834ca6044
5
+ SHA512:
6
+ metadata.gz: 015aa186bf405bd4f6b7ec43fe777cfb15fef66ec3ad3de9280717d63e8162e408284fa4e3b659bfb5dc1749c60f014ee16033620eef5132c23bb1fd9143268f
7
+ data.tar.gz: 4e87cc53f54d0320b565354aabfcd40cd3b2158fe027dfb52fd7fbc3738184e3feb5faf874a222ae6f62706f3d73e5a0e71c5ade9ce6fe31b0e230a83bdd5187
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/.rubocop.yml ADDED
@@ -0,0 +1,65 @@
1
+ plugins:
2
+ - rubocop-rspec
3
+
4
+ AllCops:
5
+ TargetRubyVersion: 3.1
6
+ NewCops: enable
7
+ SuggestExtensions: false
8
+
9
+ Style/Documentation:
10
+ Enabled: false
11
+
12
+ Style/FrozenStringLiteralComment:
13
+ Enabled: true
14
+
15
+ Metrics/MethodLength:
16
+ Max: 25
17
+
18
+ Metrics/ClassLength:
19
+ Max: 200
20
+
21
+ Metrics/BlockLength:
22
+ Exclude:
23
+ - "spec/**/*"
24
+ - "*.gemspec"
25
+
26
+ RSpec/DescribeClass:
27
+ Exclude:
28
+ - "spec/cuzk/rest/errors_spec.rb"
29
+ - "spec/integration/**/*"
30
+
31
+ Layout/LineLength:
32
+ Max: 120
33
+
34
+ RSpec/ExampleLength:
35
+ Max: 15
36
+
37
+ RSpec/MultipleExpectations:
38
+ Max: 8
39
+
40
+ RSpec/NestedGroups:
41
+ Max: 4
42
+
43
+ RSpec/SpecFilePathFormat:
44
+ Enabled: false
45
+
46
+ Naming/FileName:
47
+ Exclude:
48
+ - 'lib/cuzk-rest.rb'
49
+
50
+ Naming/MethodParameterName:
51
+ AllowedNames:
52
+ - x
53
+ - y
54
+ - id
55
+ - e
56
+
57
+ Naming/PredicateMethod:
58
+ AllowedMethods:
59
+ - validate!
60
+
61
+ Metrics/CyclomaticComplexity:
62
+ Max: 12
63
+
64
+ Metrics/AbcSize:
65
+ Max: 50
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 3.4.2
data/CHANGELOG.md ADDED
@@ -0,0 +1,22 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [0.1.0] - 2026-02-02
11
+
12
+ ### Added
13
+
14
+ - Initial release of the cuzk-rest gem
15
+ - Core client with Faraday HTTP layer, rate limiting, and retry logic
16
+ - Resource classes: Parcel, Building, Unit, Ownership
17
+ - Supporting classes: Registry, Territory, Procedure, Search
18
+ - Configuration with API key authentication
19
+ - Full error hierarchy for API, network, data, and privacy errors
20
+ - GDPR-aware privacy mode with personal data masking
21
+ - Comprehensive RSpec test suite with WebMock stubs
22
+ - CI/CD via GitHub Actions for Ruby 3.1-3.4
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 Segfault Labs
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,321 @@
1
+ # cuzk-rest
2
+
3
+ Ruby client for the [ČÚZK REST API](https://api-kn.cuzk.gov.cz/) (Czech Real Estate Registry / Katastr nemovitostí).
4
+
5
+ Provides idiomatic Ruby access to cadastral data including parcels, buildings, units, procedures, territorial registries, and application services.
6
+
7
+ ## Installation
8
+
9
+ Add to your Gemfile:
10
+
11
+ ```ruby
12
+ gem 'cuzk-rest'
13
+ ```
14
+
15
+ Or install directly:
16
+
17
+ ```sh
18
+ gem install cuzk-rest
19
+ ```
20
+
21
+ ## Requirements
22
+
23
+ - Ruby >= 3.1
24
+ - ČÚZK REST API key ([request one here](https://www.cuzk.cz/))
25
+
26
+ ## Quick Start
27
+
28
+ ```ruby
29
+ require 'cuzk-rest'
30
+
31
+ # Configure globally
32
+ CUZK::REST.configure do |config|
33
+ config.api_key = 'your-api-key'
34
+ end
35
+
36
+ # Or via environment variable CUZK_REST_API_KEY
37
+ client = CUZK::REST::Client.new
38
+ ```
39
+
40
+ ## Usage
41
+
42
+ ### Client
43
+
44
+ The `Client` class is the main entry point. It sets up the connection and provides convenience methods for all resources.
45
+
46
+ ```ruby
47
+ client = CUZK::REST::Client.new(api_key: 'your-api-key')
48
+
49
+ # Or with additional options
50
+ client = CUZK::REST::Client.new(
51
+ api_key: 'your-api-key',
52
+ timeout: 60,
53
+ retries: 5
54
+ )
55
+ ```
56
+
57
+ ### Parcels (Parcely)
58
+
59
+ ```ruby
60
+ # Find a parcel by ID
61
+ parcel = client.find_parcel(728_497)
62
+ parcel.number # => 1
63
+ parcel.full_number # => "1" or "123/4" with sub-number
64
+ parcel.area # => 56942
65
+ parcel.land_type # => "zastavěná plocha a nádvoří"
66
+ parcel.cadastral_unit_name # => "Hradčany"
67
+ parcel.lv_number # => 60
68
+
69
+ # Search parcels
70
+ parcels = client.search_parcels(cadastral_unit: '727091')
71
+ parcels = client.search_parcels(number: '123', lv_number: '60')
72
+
73
+ # Find by cadastral unit
74
+ parcels = client.find_parcels_by_cadastral_unit('727091')
75
+
76
+ # Find neighboring parcels
77
+ neighbors = client.find_neighboring_parcels(728_497)
78
+ ```
79
+
80
+ ### Buildings (Stavby)
81
+
82
+ ```ruby
83
+ # Find a building by ID
84
+ building = client.find_building(1_950_001)
85
+ building.descriptive_number # => 1
86
+ building.municipality # => "Praha"
87
+ building.street # => "Hradčanské náměstí"
88
+ building.full_address # => "Hradčanské náměstí, 1, Hradčany, Praha, 11900"
89
+
90
+ # Search buildings
91
+ buildings = client.search_buildings(house_number: '100')
92
+
93
+ # Find by address point code
94
+ building = client.find_building_by_address_point(12_345)
95
+ ```
96
+
97
+ ### Units (Jednotky)
98
+
99
+ ```ruby
100
+ # Find a unit by ID
101
+ unit = client.find_unit(3001)
102
+ unit.number # => "100/1"
103
+ unit.unit_type_description # => "byt"
104
+ unit.area # => 65
105
+ unit.ownership_share # => "65/1000"
106
+ unit.share_percentage # => 6.5
107
+
108
+ # Search units
109
+ units = client.search_units(house_number: '100', unit_number: '1')
110
+
111
+ # Find units in a building
112
+ units = client.find_units_by_building('100')
113
+ ```
114
+
115
+ ### Procedures (Rizeni)
116
+
117
+ ```ruby
118
+ # Find a procedure by ID
119
+ procedure = client.find_procedure(5001)
120
+ procedure.procedure_number # => "V-12345/2024-611"
121
+ procedure.active? # => true
122
+
123
+ # Search procedures
124
+ procedures = client.search_procedures(cadastral_unit: '727091')
125
+
126
+ # Active procedures only
127
+ procedures = client.active_procedures(cadastral_unit: '727091')
128
+
129
+ # Procedures received on a specific date
130
+ procedures = client.procedures_received_on('2024-06-15')
131
+ ```
132
+
133
+ ### Building Rights (Prava stavby)
134
+
135
+ ```ruby
136
+ # Find a building right by ID
137
+ right = client.find_building_right(6001)
138
+
139
+ # Find by parcel or building
140
+ rights = client.find_building_rights_by_parcel(1001)
141
+ rights = client.find_building_rights_by_building(2001)
142
+ ```
143
+
144
+ ### Registries (Ciselniky ISKN)
145
+
146
+ ```ruby
147
+ registries = client.registries
148
+
149
+ # Fetch registry lists
150
+ land_types = registries.land_types
151
+ building_types = registries.building_types
152
+ unit_types = registries.unit_types
153
+ protection_types = registries.protection_types
154
+ land_use_types = registries.land_use_types
155
+
156
+ # Translate codes
157
+ name = registries.translate_code(2, registry_type: 'DruhyPozemku')
158
+ # => "orná půda"
159
+
160
+ # Find code by description
161
+ code = registries.find_code('vinice', registry_type: 'DruhyPozemku')
162
+ # => 3
163
+ ```
164
+
165
+ ### Territories (Ciselniky uzemních jednotek)
166
+
167
+ ```ruby
168
+ # Regions (Kraje)
169
+ regions = client.regions # => all 14 Czech regions
170
+
171
+ # Districts (Okresy)
172
+ districts = client.districts(region: '116')
173
+
174
+ # Municipalities (Obce)
175
+ municipalities = client.municipalities(district: '3702')
176
+
177
+ # Cadastral units (Katastralni uzemi)
178
+ units = client.cadastral_units(municipality: '582786')
179
+ ```
180
+
181
+ ### Health Check
182
+
183
+ ```ruby
184
+ # Test connectivity (no auth required)
185
+ result = client.health
186
+ result.status # => "Healthy"
187
+
188
+ # Simple boolean check
189
+ client.test_connection # => true / false
190
+ ```
191
+
192
+ ## Configuration
193
+
194
+ ```ruby
195
+ CUZK::REST.configure do |config|
196
+ # Required
197
+ config.api_key = 'your-api-key'
198
+
199
+ # Optional (shown with defaults)
200
+ config.base_url = 'https://api-kn.cuzk.gov.cz'
201
+ config.api_version = 'v1'
202
+ config.timeout = 30 # request timeout in seconds
203
+ config.open_timeout = 10 # connection open timeout
204
+ config.retries = 3 # retry count for 429/5xx
205
+ config.retry_interval = 0.5 # seconds between retries
206
+ config.requests_per_minute = 60
207
+ config.burst_limit = 10
208
+
209
+ # Privacy
210
+ config.mask_personal_data = true # mask owner names (GDPR)
211
+ config.privacy_mode = :strict
212
+
213
+ # Logging
214
+ config.logger = Logger.new($stdout)
215
+ config.log_requests = true
216
+ config.log_responses = true
217
+ end
218
+ ```
219
+
220
+ The API key can also be set via the `CUZK_REST_API_KEY` environment variable.
221
+
222
+ ## Error Handling
223
+
224
+ All API errors inherit from `CUZK::REST::Error`:
225
+
226
+ ```ruby
227
+ begin
228
+ client.find_parcel(9999)
229
+ rescue CUZK::REST::NotFoundError => e
230
+ puts "Not found: #{e.message}"
231
+ rescue CUZK::REST::AuthenticationError
232
+ puts "Invalid API key"
233
+ rescue CUZK::REST::RateLimitedError
234
+ puts "Rate limit exceeded, try again later"
235
+ rescue CUZK::REST::TimeoutError
236
+ puts "Request timed out"
237
+ rescue CUZK::REST::APIError => e
238
+ puts "API error #{e.status}: #{e.message}"
239
+ end
240
+ ```
241
+
242
+ Error hierarchy:
243
+
244
+ ```
245
+ Error
246
+ ├── ConfigurationError
247
+ ├── APIError
248
+ │ ├── AuthenticationError (401)
249
+ │ ├── AuthorizationError (403)
250
+ │ ├── NotFoundError (404)
251
+ │ ├── ValidationError (422)
252
+ │ ├── RateLimitedError (429)
253
+ │ └── ServerError (5xx)
254
+ ├── NetworkError
255
+ │ ├── TimeoutError
256
+ │ └── ConnectionError
257
+ ├── DataError
258
+ │ ├── ParsingError
259
+ │ └── InvalidResponseError
260
+ └── PrivacyError
261
+ └── GDPRViolationError
262
+ ```
263
+
264
+ ## Response Metadata
265
+
266
+ The ČÚZK API wraps responses in an envelope containing metadata. After any request, you can access it via the connection:
267
+
268
+ ```ruby
269
+ client.find_parcel(728_497)
270
+ metadata = client.connection.last_response_metadata
271
+ metadata['zpravy'] # => [] (messages/warnings)
272
+ metadata['aktualnostDatK'] # => "2024-12-01T00:00:00" (data currency)
273
+ metadata['provedenoVolani'] # => 1.0 (call count)
274
+ ```
275
+
276
+ ## API Endpoints
277
+
278
+ This gem maps to the following ČÚZK REST API endpoints:
279
+
280
+ | Resource | API Path | Methods |
281
+ |---|---|---|
282
+ | Parcel | `Parcely` | find, search (Vyhledani), SousedniParcely |
283
+ | Building | `Stavby` | find, search (Vyhledani), AdresniMisto |
284
+ | Unit | `Jednotky` | find, search (Vyhledani) |
285
+ | Procedure | `Rizeni` | find, search (Vyhledani), PrijateDne |
286
+ | Building Right | `PravaStavby` | find, Parcela, Stavba |
287
+ | Registry | `CiselnikyISKN` | DruhyPozemku, TypyStavby, TypyJednotky, ... |
288
+ | Territory | `CiselnikyUzemnichJednotek` | Kraje, Okresy, Obce, CastiObci, KatastralniUzemi |
289
+ | App Service | `AplikacniSluzby` | Health, StavUctu, AktualnostDat |
290
+
291
+ ## Development
292
+
293
+ ```sh
294
+ # Install dependencies
295
+ bundle install
296
+
297
+ # Run tests
298
+ bundle exec rspec
299
+
300
+ # Run only unit tests (no integration)
301
+ bundle exec rspec --exclude-pattern 'spec/integration/**/*'
302
+
303
+ # Run integration tests (VCR cassettes)
304
+ bundle exec rspec spec/integration/ --tag ~live
305
+
306
+ # Run live health check (requires API key)
307
+ CUZK_REST_API_KEY=your-key bundle exec rspec spec/integration/ --tag live
308
+
309
+ # Lint
310
+ bundle exec rubocop
311
+ ```
312
+
313
+ ## License
314
+
315
+ MIT License. See [LICENSE](LICENSE) for details.
316
+
317
+ ## Links
318
+
319
+ - [ČÚZK REST API](https://api-kn.cuzk.gov.cz/)
320
+ - [API Swagger Docs](https://api-kn.cuzk.gov.cz/swagger/v1.0/swagger.json)
321
+ - [ČÚZK (Czech Office for Surveying, Mapping and Cadastre)](https://www.cuzk.cz/)
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ require 'rspec/core/rake_task'
5
+ require 'rubocop/rake_task'
6
+
7
+ RSpec::Core::RakeTask.new(:spec)
8
+ RuboCop::RakeTask.new
9
+
10
+ task default: %i[spec rubocop]
@@ -0,0 +1,209 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CUZK
4
+ module REST
5
+ class Client
6
+ attr_reader :config, :connection
7
+
8
+ class << self
9
+ def default_connection
10
+ @default_connection || raise(ConfigurationError, 'No client configured. Use CUZK::REST::Client.new first.')
11
+ end
12
+
13
+ attr_writer :default_connection
14
+ end
15
+
16
+ def initialize(api_key: nil, **options)
17
+ @config = build_config(api_key, options)
18
+ @connection = Connection.new(@config)
19
+ assign_connection_to_resources
20
+
21
+ yield self if block_given?
22
+ end
23
+
24
+ # --- Parcel Operations ---
25
+
26
+ def find_parcel(parcel_id)
27
+ Resources::Parcel.find(parcel_id)
28
+ end
29
+
30
+ def search_parcels(params = {})
31
+ Resources::Parcel.search(params)
32
+ end
33
+
34
+ def find_parcels_by_cadastral_unit(cadastral_unit_code, **params)
35
+ Resources::Parcel.find_by_cadastral_unit(cadastral_unit_code, params)
36
+ end
37
+
38
+ def find_neighboring_parcels(parcel_id)
39
+ Resources::Parcel.find_neighbors(parcel_id)
40
+ end
41
+
42
+ # --- Building Operations ---
43
+
44
+ def find_building(building_id)
45
+ Resources::Building.find(building_id)
46
+ end
47
+
48
+ def search_buildings(params = {})
49
+ Resources::Building.search(params)
50
+ end
51
+
52
+ def find_building_by_address_point(address_code)
53
+ Resources::Building.find_by_address_point(address_code)
54
+ end
55
+
56
+ # --- Unit Operations ---
57
+
58
+ def find_unit(unit_id)
59
+ Resources::Unit.find(unit_id)
60
+ end
61
+
62
+ def search_units(params = {})
63
+ Resources::Unit.search(params)
64
+ end
65
+
66
+ def find_units_by_building(building_id)
67
+ Resources::Unit.find_by_building(building_id)
68
+ end
69
+
70
+ # --- Procedure Operations ---
71
+
72
+ def find_procedure(procedure_id)
73
+ Resources::Procedure.find(procedure_id)
74
+ end
75
+
76
+ def search_procedures(params = {})
77
+ Resources::Procedure.search(params)
78
+ end
79
+
80
+ def active_procedures(**params)
81
+ Resources::Procedure.active(params)
82
+ end
83
+
84
+ def procedures_received_on(date, **params)
85
+ Resources::Procedure.received_on(date, params)
86
+ end
87
+
88
+ # --- Building Right Operations ---
89
+
90
+ def find_building_right(id)
91
+ Resources::BuildingRight.find(id)
92
+ end
93
+
94
+ def find_building_rights_by_parcel(parcel_id)
95
+ Resources::BuildingRight.find_by_parcel(parcel_id)
96
+ end
97
+
98
+ def find_building_rights_by_building(building_id)
99
+ Resources::BuildingRight.find_by_building(building_id)
100
+ end
101
+
102
+ # --- Registry Operations ---
103
+
104
+ def registries
105
+ @registries ||= RegistryAccessor.new
106
+ end
107
+
108
+ # --- Territory Operations ---
109
+
110
+ def cadastral_units(**params)
111
+ Resources::Territory.cadastral_units(params)
112
+ end
113
+
114
+ def municipalities(**params)
115
+ Resources::Territory.municipalities(params)
116
+ end
117
+
118
+ def districts(**params)
119
+ Resources::Territory.districts(params)
120
+ end
121
+
122
+ def regions
123
+ Resources::Territory.regions
124
+ end
125
+
126
+ # --- App Service Operations ---
127
+
128
+ def health
129
+ Resources::AppService.health
130
+ end
131
+
132
+ # --- Connection Info ---
133
+
134
+ def test_connection
135
+ health
136
+ true
137
+ rescue Error
138
+ false
139
+ end
140
+
141
+ def connected?
142
+ !@connection.nil?
143
+ end
144
+
145
+ private
146
+
147
+ def build_config(api_key, options)
148
+ config = CUZK::REST.configuration.dup
149
+ config.api_key = api_key if api_key
150
+ options.each { |key, value| config.public_send(:"#{key}=", value) }
151
+ config
152
+ end
153
+
154
+ def assign_connection_to_resources
155
+ self.class.default_connection = @connection
156
+ resource_classes.each { |klass| klass.connection = @connection }
157
+ end
158
+
159
+ def resource_classes
160
+ [
161
+ Resources::Parcel,
162
+ Resources::Building,
163
+ Resources::Unit,
164
+ Resources::Ownership,
165
+ Resources::Registry,
166
+ Resources::Territory,
167
+ Resources::Procedure,
168
+ Resources::AppService,
169
+ Resources::BuildingRight
170
+ ]
171
+ end
172
+
173
+ # Convenience wrapper for registry access
174
+ class RegistryAccessor
175
+ def land_types
176
+ Resources::Registry.land_types
177
+ end
178
+
179
+ def building_types
180
+ Resources::Registry.building_types
181
+ end
182
+
183
+ def building_purposes
184
+ Resources::Registry.building_purposes
185
+ end
186
+
187
+ def unit_types
188
+ Resources::Registry.unit_types
189
+ end
190
+
191
+ def protection_types
192
+ Resources::Registry.protection_types
193
+ end
194
+
195
+ def land_use_types
196
+ Resources::Registry.land_use_types
197
+ end
198
+
199
+ def translate_code(code, registry_type:)
200
+ Resources::Registry.translate_code(code, registry_type: registry_type)
201
+ end
202
+
203
+ def find_code(description, registry_type:)
204
+ Resources::Registry.find_code(description, registry_type: registry_type)
205
+ end
206
+ end
207
+ end
208
+ end
209
+ end