epok 0.2.0 → 0.6.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 12d0e1ef79e2fd5ff522e79748f413cdb123a79c863bba24a85dce868a1c7127
4
- data.tar.gz: 0c44daa4fe8d10908c75301d9a363bacf4d03ff0edf0eb13a2ff307cf96fe286
3
+ metadata.gz: 74780db81aad8442533598aab22a695b4565710ce30ab5504439f59e55587195
4
+ data.tar.gz: d3562a616e0f3d28a666041f43abaa770e0bdbf386b581f64f5d225ccc6cbbd4
5
5
  SHA512:
6
- metadata.gz: 8358374a58c28bd2cb5f10ec9ce94248b0a9ba2a2bb2a733f4cfb003a62fecf90e8e6ec46ffd8648b8ab4172df745976586fcc09b264d10c26f34438138af961
7
- data.tar.gz: bcc962b28eaf4a3d8bb62325a422709488a437338aad6c7eced0afe9bce2c27a5865f7d367674759888e7fe78c28f46d8fb4350b3424dab60fba894627050816
6
+ metadata.gz: 02e22d2fb37d7b44611461af1bcc7e006b6a97d78c54a07416f092e9a00a989b26bd6a0bc38a6558378a200fb177e7c921a54f970e388711fa520888ab71f16e
7
+ data.tar.gz: 637d65db4058187386da2b632685b1488749eb859689bfaf242d962803d05fbfd1147f78d41256ea005445307e3ec8e5258c7b4c9437c2187181ddd5e9b4dfc7
data/CHANGELOG.md ADDED
@@ -0,0 +1,97 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here. The format follows
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the project
5
+ follows [Semantic Versioning](https://semver.org/).
6
+
7
+ ## [Unreleased]
8
+
9
+ ## [0.6.0] - 2026-09-15
10
+
11
+ ### Changed
12
+
13
+ - **Breaking:** `Epok.geocoder` is now `Epok.nearby`. Next to `Epok.geocode`
14
+ (address to point) the old name read as the same thing backwards.
15
+ - Every string in an API response is stripped and squeezed once in `API.get`.
16
+ The API pads and double-spaces names at random, in every endpoint, and the
17
+ per-field strips it replaces only covered some of them.
18
+ - Gemspec declares `required_ruby_version >= 3.3`, links the changelog and
19
+ source on rubygems.org, and ships only `lib/`, README, CHANGELOG and
20
+ LICENSE instead of the whole repository (tests and cassettes included).
21
+
22
+ ### Fixed
23
+
24
+ - Malformed responses no longer escape as `NoMethodError`: an unparseable
25
+ centroid raises `Epok::Error`, non-string content values are kept as
26
+ strings, and a geocode match without coordinates has a nil `location`.
27
+
28
+ ## [0.5.0] - 2026-09-15
29
+
30
+ ### Added
31
+
32
+ - `Epok.geocode(text)` turns "cabildo 1234" or "callao y corrientes" into
33
+ `Address` structs with the normalized text, partido, localidad and a
34
+ `Location`, via USIG's normalizer. Unknown addresses raise `Epok::NotFound`
35
+ with the service's message.
36
+ - `Epok.datos_utiles(location)` returns the barrio, comuna, comisaría,
37
+ hospital area, health region and school district for a point. Outside the
38
+ city those are nil and `partido_amba` and `localidad_amba` are filled.
39
+ - `Epok.search` accepts `categories:` (one or an array) and `limit:`.
40
+
41
+ ### Changed
42
+
43
+ - README rewritten around an end-to-end example.
44
+ - Removed minitest-reporters from the development dependencies.
45
+
46
+ ## [0.4.0] - 2026-09-15
47
+
48
+ ### Added
49
+
50
+ - `Epok::Object` keeps listing data: `name`, `kind`, `category` and
51
+ `distance` (metres, from the geocoder) are available without a request.
52
+ `content`, `normalized_address` and `location` fetch lazily.
53
+ - `Epok::Object#location` returns longitude and latitude, converted from the
54
+ city's projected grid through USIG. Nil when the object has no centroid.
55
+ - `Epok.categories` lists all EPOk categories as `Category` structs.
56
+ - `Epok.geocoder` accepts `radius:` (default 500 metres) and an array of
57
+ categories.
58
+ - `Epok::Error` wraps timeouts, network failures, non-2xx responses and bad
59
+ JSON. `Epok::NotFound` for unknown ids.
60
+
61
+ ## [0.3.0] - 2026-09-15
62
+
63
+ Tagged but never pushed to rubygems.org.
64
+
65
+ ### Changed
66
+
67
+ - **Breaking:** `Epok.search` and `Epok.geocoder` return an
68
+ `Epok::Collection`, which is `Enumerable` and lazy. `Epok::Search`,
69
+ `Epok::Geocoder` and `Epok::Collectable` are removed, along with the
70
+ `query`, `x`, `y` and `categories` readers.
71
+
72
+ ### Fixed
73
+
74
+ - Query parameters are URL-encoded, so non-ASCII searches like "peña" no
75
+ longer raise `URI::InvalidURIError`.
76
+
77
+ ## [0.2.0] - 2026-09-15
78
+
79
+ ### Fixed
80
+
81
+ - Requests go over HTTPS. The API started redirecting plain HTTP to HTTPS
82
+ with an empty body, which made every call fail with a JSON parse error.
83
+ - Object content values are stripped of surrounding whitespace.
84
+ - Test suite runs on Ruby 4: vcr bumped to 6.x, lockfile regenerated,
85
+ bundler dev dependency dropped.
86
+
87
+ ## [0.1.1] - 2019-04-24
88
+
89
+ Initial release: `Epok.search`, `Epok.geocoder` and `Epok::Object`.
90
+
91
+ [Unreleased]: https://github.com/arzezak/epok/compare/v0.6.0...HEAD
92
+ [0.6.0]: https://github.com/arzezak/epok/compare/v0.5.0...v0.6.0
93
+ [0.5.0]: https://github.com/arzezak/epok/compare/v0.4.0...v0.5.0
94
+ [0.4.0]: https://github.com/arzezak/epok/compare/v0.3.0...v0.4.0
95
+ [0.3.0]: https://github.com/arzezak/epok/compare/v0.2.0...v0.3.0
96
+ [0.2.0]: https://github.com/arzezak/epok/compare/e629f99...v0.2.0
97
+ [0.1.1]: https://github.com/arzezak/epok/commits/e629f99
data/README.md CHANGED
@@ -20,29 +20,73 @@ Or install it yourself as:
20
20
 
21
21
  ## Usage
22
22
 
23
+ "I'm at Callao y Corrientes, what's around me?" in five requests:
24
+
23
25
  ```ruby
24
- >> obelisco = Epok::Location.new(x: -58.381570, y: -34.603738)
25
- => #<struct Epok::Location x=-58.38157, y=-34.603738>
26
+ require "epok"
27
+
28
+ # 1. Turn a typed address into coordinates.
29
+ here = Epok.geocode("callao y corrientes").first
30
+ puts "You are at #{here.address}"
31
+
32
+ # 2. Which neighbourhood and comuna is that?
33
+ area = Epok.datos_utiles(here.location)
34
+ puts "That's #{area.barrio}, #{area.comuna}"
35
+
36
+ # 3. What's within 300 metres, by category?
37
+ nearby = Epok.nearby(here.location, %w[estaciones_de_subte farmacias], radius: 300)
38
+ nearby.sort_by(&:distance).first(5).each do |place|
39
+ puts " #{place.distance.round} m #{place.name} (#{place.kind})"
40
+ end
41
+
42
+ # 4. Drill into one: full record and coordinates for a map pin.
43
+ closest = nearby.min_by(&:distance)
44
+ puts "Pin: #{closest.location.y}, #{closest.location.x}"
45
+ closest.content.each { |key, value| puts " #{key}: #{value}" }
46
+ ```
47
+
48
+ ```
49
+ You are at CALLAO AV. y CORRIENTES AV., CABA
50
+ That's San Nicolas, Comuna 3
51
+ 3 m CALLAO - MAESTRO ALFREDO BRAVO (Línea B) (Estación de Subte (Metro))
52
+ 42 m Farmacia en CORRIENTES AV. 1820 (Farmacia)
53
+ 73 m Farmacia en CORRIENTES AV. 1835 (Farmacia)
54
+ 98 m Farmacia en CALLAO AV. 477 (Farmacia)
55
+ 111 m Farmacia en CORRIENTES AV. 1880 (Farmacia)
56
+ Pin: -34.604419, -58.392318
57
+ Nombre: CALLAO - MAESTRO ALFREDO BRAVO (Línea B)
58
+ Línea: B
59
+ Estado: Estación Malabia cerrada por obras de renovación integral.
60
+ Cabeceras: J. M. de Rosas - L. N. Alem
61
+ Tiempo: 27 min.
62
+ ```
26
63
 
27
- >> pharmacies_around_obelisco = Epok.geocoder(obelisco, "farmacias")
28
- => #<Epok::Geocoder:0x00007f8719ab4f08 @x=-58.38157, @y=-34.603738, @categories="farmacias">
64
+ Steps 1 to 3 are one request each; step 4 is two (the record, then the coordinate conversion). The content hash is whatever the city publishes for that category: subway stations carry line, status and travel time, pharmacies carry phone and delivery details.
29
65
 
30
- >> pharmacies_around_obelisco.first.content
31
- => {"Nombre"=>"Farmacia en PELLEGRINI, CARLOS 765", "Teléfono"=>"43223198 43220298"}
66
+ ### The rest
32
67
 
33
- >> cgp = Epok.search("cgp")
34
- => #<Epok::Search:0x00007f871a09ac60 @query="cgp">
68
+ **Search by text**, optionally filtered by category and capped:
35
69
 
36
- >> sede_4 = cgp.first
37
- => #<Epok::Object:0x00007f8719b36788 @id="sedes_de_comunas|4">
70
+ ```ruby
71
+ Epok.search("cgp", categories: "sedes_de_comunas", limit: 3).map(&:name)
72
+ # => ["Sede Comunal 4", "Sede Comunal 9", "Sede Comunal 10"]
73
+ ```
38
74
 
39
- >> sede_4.name
40
- => "Sede Comunal 4"
75
+ **Categories** are what `nearby` and the search filter take. There are 167, each with an id, a display name and a description:
41
76
 
42
- >> sede_4.content
43
- => {"Nombre"=>"Sede Comunal 4", "Dirección"=>"BARCO CENTENERA del 2906", "Teléfonos"=>"4918-2243/1815/8920-4949 9024 Int. 105", "Trámites y Servicios"=>"<a href=\"http://www.buenosaires.gob.ar/comuna-4/sede-comunal-4\" target=\"_blank\">link</a>"}
77
+ ```ruby
78
+ Epok.categories.find { |c| c.id == "farmacias" }
79
+ # => #<struct Epok::Category id="farmacias", name="Farmacias", description="">
44
80
  ```
45
81
 
82
+ **Collections** from `search` and `nearby` are `Enumerable` and lazy: nothing is requested until you iterate.
83
+
84
+ **Objects** know their `id`, `name`, `kind`, `category` and, from `nearby`, `distance` in metres straight from the listing. `content`, `normalized_address` and `location` fetch the full record on first use; `location` also converts the city's projected coordinates through USIG. `Epok::Object.new("farmacias|1082")` looks one up by id.
85
+
86
+ **Locations** are `Epok::Location.new(x: longitude, y: latitude)`. `geocode` returns every match across the metro area, city first, as `Address` structs with `address`, `partido`, `localidad` and `location`. `datos_utiles` returns nil for the city fields outside CABA and fills `partido_amba` and `localidad_amba` instead.
87
+
88
+ **Errors**: every request failure raises `Epok::Error`. An unknown id or address raises `Epok::NotFound`.
89
+
46
90
  ## Development
47
91
 
48
92
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
data/lib/epok/api.rb CHANGED
@@ -2,26 +2,87 @@ require "net/http"
2
2
  require "json"
3
3
 
4
4
  module Epok
5
+ class Error < StandardError; end
6
+ class NotFound < Error; end
7
+
5
8
  class API
6
- BASE_URL = "https://epok.buenosaires.gob.ar".freeze
9
+ EPOK_URL = "https://epok.buenosaires.gob.ar".freeze
10
+ USIG_URL = "https://ws.usig.buenosaires.gob.ar".freeze
11
+ USIG_SERVICES_URL = "https://servicios.usig.buenosaires.gob.ar".freeze
12
+ TIMEOUT = 10
7
13
 
8
14
  def self.object(id)
9
- JSON.parse(get("/getObjectContent/?id=#{id}"))
15
+ object = get(EPOK_URL, "/getObjectContent/", id: id)
16
+ raise NotFound, "no object with id #{id}" if object.empty?
17
+
18
+ object
19
+ end
20
+
21
+ def self.search(query, categories = nil, limit = nil)
22
+ params = { texto: query, categoria: categories, limit: limit }.compact
23
+
24
+ get(EPOK_URL, "/buscar/", params)["instancias"]
25
+ end
26
+
27
+ def self.nearby(x, y, categories, radius)
28
+ get(EPOK_URL, "/reverseGeocoderLugares/",
29
+ x: x, y: y, categorias: categories, radio: radius)["instancias"]
30
+ end
31
+
32
+ def self.categories
33
+ get(EPOK_URL, "/getCategorias/", {})["categorias"]
10
34
  end
11
35
 
12
- def self.search(query)
13
- JSON.parse(get("/buscar/?texto=#{query}"))["instancias"]
36
+ def self.geocode(text)
37
+ response = get(USIG_SERVICES_URL, "/normalizar/", direccion: text, geocodificar: true)
38
+ addresses = response["direccionesNormalizadas"]
39
+ raise NotFound, response["errorMessage"] if addresses.empty?
40
+
41
+ addresses
14
42
  end
15
43
 
16
- def self.geocoder(x, y, categories)
17
- JSON.parse(get(
18
- "/reverseGeocoderLugares/?x=#{x}&y=#{y}&categorias=#{categories}&radio=500"
19
- ))["instancias"]
44
+ def self.datos_utiles(x, y)
45
+ get(USIG_URL, "/datos_utiles", x: x, y: y)
20
46
  end
21
47
 
22
- def self.get(path)
23
- Net::HTTP.get_response(URI("#{BASE_URL}#{path}")).body
48
+ # Converts EPOk's projected coordinates (Gauss-Krüger Buenos Aires) to
49
+ # longitude and latitude through the city's USIG service.
50
+ def self.to_lonlat(x, y)
51
+ response = get(USIG_URL, "/rest/convertir_coordenadas", x: x, y: y, output: "lonlat")
52
+ raise Error, "USIG could not convert (#{x}, #{y})" unless response["tipo_resultado"] == "Ok"
53
+
54
+ response["resultado"].values_at("x", "y").map(&:to_f)
55
+ end
56
+
57
+ def self.get(host, path, params)
58
+ uri = URI("#{host}#{path}")
59
+ uri.query = URI.encode_www_form(params)
60
+
61
+ response = Net::HTTP.start(uri.host, uri.port, use_ssl: true,
62
+ open_timeout: TIMEOUT, read_timeout: TIMEOUT) do |http|
63
+ http.get(uri.request_uri)
64
+ end
65
+
66
+ unless response.is_a?(Net::HTTPSuccess)
67
+ raise Error, "EPOk responded #{response.code} to #{uri}"
68
+ end
69
+
70
+ normalize(JSON.parse(response.body))
71
+ rescue SystemCallError, SocketError, Timeout::Error, OpenSSL::SSL::SSLError, JSON::ParserError => e
72
+ raise Error, "#{e.class}: #{e.message}"
24
73
  end
25
74
  private_class_method :get
75
+
76
+ # The API pads and double-spaces strings at random. Clean every string
77
+ # in a response once, here, so no field needs its own strip.
78
+ def self.normalize(value)
79
+ case value
80
+ when Hash then value.transform_values { |v| normalize(v) }
81
+ when Array then value.map { |v| normalize(v) }
82
+ when String then value.strip.squeeze(" ")
83
+ else value
84
+ end
85
+ end
86
+ private_class_method :normalize
26
87
  end
27
88
  end
@@ -2,22 +2,18 @@ module Epok
2
2
  class Collection
3
3
  include Enumerable
4
4
 
5
- attr_reader :data
6
-
7
- def initialize(data, id = "id")
8
- @data = collection(data, id)
5
+ def initialize(&fetch)
6
+ @fetch = fetch
9
7
  end
10
8
 
11
9
  def each(&block)
12
- data.each(&block)
10
+ objects.each(&block)
13
11
  end
14
12
 
15
13
  private
16
14
 
17
- def collection(data, id)
18
- data.map do |item|
19
- Object.new(item[id])
20
- end
15
+ def objects
16
+ @objects ||= @fetch.call.map { |item| Object.new(item) }
21
17
  end
22
18
  end
23
19
  end
data/lib/epok/object.rb CHANGED
@@ -1,23 +1,54 @@
1
1
  module Epok
2
2
  class Object
3
- attr_reader :id
3
+ attr_reader :id, :distance
4
4
 
5
- def initialize(id)
6
- @id = id
5
+ # Accepts either an id ("farmacias|1082") or a result hash from a search
6
+ # or nearby listing. Listing attributes are available immediately; the
7
+ # full content is fetched on first use.
8
+ def initialize(attributes)
9
+ attributes = { "id" => attributes } if attributes.is_a?(String)
10
+
11
+ @id = attributes.fetch("id")
12
+ @name = attributes["nombre"]
13
+ @kind = attributes["clase"]
14
+ @distance = attributes["distancia"]&.to_f
7
15
  end
8
16
 
9
17
  def name
10
- content["Nombre"]
18
+ @name ||= content["Nombre"]
19
+ end
20
+
21
+ def kind
22
+ @kind ||= object["clase"]
23
+ end
24
+
25
+ def category
26
+ id.split("|").first
11
27
  end
12
28
 
13
29
  def normalized_address
14
30
  object["direccionNormalizada"]
15
31
  end
16
32
 
33
+ # Longitude and latitude as a Location, or nil when EPOk has no
34
+ # centroid for the object. Costs one extra request the first time.
35
+ def location
36
+ return @location if defined?(@location)
37
+
38
+ centroid = object.dig("ubicacion", "centroide")
39
+ @location = centroid && begin
40
+ point = centroid[/\(([^)]*)\)/, 1]
41
+ raise Error, "unparseable centroid #{centroid.inspect} for #{id}" unless point
42
+
43
+ lon, lat = API.to_lonlat(*point.split.map(&:to_f))
44
+ Location.new(x: lon, y: lat)
45
+ end
46
+ end
47
+
17
48
  def content
18
49
  object["contenido"].each_with_object({}) do |entry, hash|
19
50
  name, value = entry.values_at("nombre", "valor")
20
- value = value.strip
51
+ value = value.to_s
21
52
  hash[name] = value unless value.empty?
22
53
  end
23
54
  end
data/lib/epok/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Epok
2
- VERSION = "0.2.0"
2
+ VERSION = "0.6.0"
3
3
  end
data/lib/epok.rb CHANGED
@@ -1,17 +1,68 @@
1
1
  require "epok/api"
2
- require "epok/collectable"
3
2
  require "epok/collection"
4
- require "epok/geocoder"
5
3
  require "epok/object"
6
- require "epok/search"
7
4
  require "epok/version"
8
5
 
9
6
  module Epok
10
- def self.search(query)
11
- Search.new(query)
7
+ Location = Struct.new(:x, :y, keyword_init: true)
8
+ Category = Struct.new(:id, :name, :description, keyword_init: true)
9
+ DatosUtiles = Struct.new(
10
+ :barrio, :comuna, :comisaria, :comisaria_vecinal,
11
+ :area_hospitalaria, :region_sanitaria, :distrito_escolar,
12
+ :partido_amba, :localidad_amba,
13
+ keyword_init: true
14
+ )
15
+
16
+ Address = Struct.new(:address, :partido, :localidad, :location, keyword_init: true)
17
+
18
+ DEFAULT_RADIUS = 500
19
+
20
+ def self.search(query, categories: nil, limit: nil)
21
+ categories = Array(categories).join(",") if categories
22
+
23
+ Collection.new { API.search(query, categories, limit) }
24
+ end
25
+
26
+ def self.nearby(location, categories, radius: DEFAULT_RADIUS)
27
+ categories = Array(categories).join(",")
28
+
29
+ Collection.new { API.nearby(location.x, location.y, categories, radius) }
30
+ end
31
+
32
+ def self.geocode(text)
33
+ API.geocode(text).map do |address|
34
+ coordinates = address["coordenadas"]
35
+ location = coordinates && Location.new(
36
+ x: coordinates["x"].to_f, y: coordinates["y"].to_f
37
+ )
38
+
39
+ Address.new(
40
+ address: address["direccion"],
41
+ partido: address["nombre_partido"],
42
+ localidad: address["nombre_localidad"],
43
+ location: location
44
+ )
45
+ end
46
+ end
47
+
48
+ def self.datos_utiles(location)
49
+ datos = API.datos_utiles(location.x, location.y)
50
+
51
+ DatosUtiles.new(
52
+ DatosUtiles.members.to_h do |member|
53
+ value = datos[member.to_s]
54
+ [member, value.to_s.empty? ? nil : value]
55
+ end
56
+ )
12
57
  end
13
58
 
14
- def self.geocoder(location, categories)
15
- Geocoder.new(location, categories)
59
+ def self.categories
60
+ API.categories.map do |category|
61
+ Category.new(
62
+ id: category["nombre_normalizado"],
63
+ name: category["nombre"],
64
+ description: category["descripcion"]
65
+ )
66
+ end
16
67
  end
17
68
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: epok
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ariel Rzezak
@@ -23,26 +23,6 @@ dependencies:
23
23
  - - "~>"
24
24
  - !ruby/object:Gem::Version
25
25
  version: '5.0'
26
- - !ruby/object:Gem::Dependency
27
- name: minitest-reporters
28
- requirement: !ruby/object:Gem::Requirement
29
- requirements:
30
- - - "~>"
31
- - !ruby/object:Gem::Version
32
- version: '1.3'
33
- - - ">="
34
- - !ruby/object:Gem::Version
35
- version: 1.3.6
36
- type: :development
37
- prerelease: false
38
- version_requirements: !ruby/object:Gem::Requirement
39
- requirements:
40
- - - "~>"
41
- - !ruby/object:Gem::Version
42
- version: '1.3'
43
- - - ">="
44
- - !ruby/object:Gem::Version
45
- version: 1.3.6
46
26
  - !ruby/object:Gem::Dependency
47
27
  name: rake
48
28
  requirement: !ruby/object:Gem::Requirement
@@ -91,42 +71,31 @@ dependencies:
91
71
  - - ">="
92
72
  - !ruby/object:Gem::Version
93
73
  version: 3.5.1
94
- description: GCBA EPOk wrapper to query indexed geographic objects.
74
+ description: Search the city's indexed geographic objects, find what is near a point,
75
+ geocode addresses and look up the barrio and comuna of a location, through the GCBA
76
+ EPOk and USIG services.
95
77
  email:
96
78
  - arzezak@gmail.com
97
79
  executables: []
98
80
  extensions: []
99
81
  extra_rdoc_files: []
100
82
  files:
101
- - ".claude/skills/swarf/SKILL.md"
102
- - ".gitignore"
103
- - CLAUDE.md
104
- - CODE_OF_CONDUCT.md
105
- - Gemfile
106
- - Gemfile.lock
83
+ - CHANGELOG.md
107
84
  - LICENSE.txt
108
85
  - README.md
109
- - Rakefile
110
- - bin/console
111
- - bin/setup
112
- - epok.gemspec
113
86
  - lib/epok.rb
114
87
  - lib/epok/api.rb
115
- - lib/epok/collectable.rb
116
88
  - lib/epok/collection.rb
117
- - lib/epok/geocoder.rb
118
89
  - lib/epok/object.rb
119
- - lib/epok/search.rb
120
90
  - lib/epok/version.rb
121
- - test/epok/geocoder_test.rb
122
- - test/epok/object_test.rb
123
- - test/epok/search_test.rb
124
- - test/epok_test.rb
125
- - test/test_helper.rb
126
91
  homepage: https://github.com/arzezak/epok
127
92
  licenses:
128
93
  - MIT
129
- metadata: {}
94
+ metadata:
95
+ source_code_uri: https://github.com/arzezak/epok
96
+ changelog_uri: https://github.com/arzezak/epok/blob/main/CHANGELOG.md
97
+ bug_tracker_uri: https://github.com/arzezak/epok/issues
98
+ rubygems_mfa_required: 'true'
130
99
  rdoc_options: []
131
100
  require_paths:
132
101
  - lib
@@ -134,7 +103,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
134
103
  requirements:
135
104
  - - ">="
136
105
  - !ruby/object:Gem::Version
137
- version: '0'
106
+ version: '3.3'
138
107
  required_rubygems_version: !ruby/object:Gem::Requirement
139
108
  requirements:
140
109
  - - ">="
@@ -143,5 +112,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
143
112
  requirements: []
144
113
  rubygems_version: 4.0.20
145
114
  specification_version: 4
146
- summary: GCBA EPOk wrapper.
115
+ summary: Ruby client for the Buenos Aires city EPOk geographic API.
147
116
  test_files: []
@@ -1,77 +0,0 @@
1
- ---
2
- name: swarf
3
- description: Score Ruby methods by complexity against test coverage with swarf, and turn the worst rows into the next test to write. Use after writing or changing Ruby code and before calling a change done.
4
- ---
5
-
6
- # swarf
7
-
8
- swarf scores every Ruby method with `CRAP = CC² · (1 − coverage)³ + CC` and lists them worst first. Complexity is parsed from source. Coverage is recorded by a probe loaded into the test run and stored in `.swarf/coverage.json`, so scoring works without setup but coverage needs a test run.
9
-
10
- ## The loop
11
-
12
- 1. Run the suite with the probe loaded (see Setup if unsure):
13
-
14
- ```
15
- bundle exec rspec # or: bundle exec rake test
16
- ```
17
-
18
- 2. Score the files you touched, not the whole project:
19
-
20
- ```
21
- git diff --name-only --diff-filter=d main -- '*.rb' | xargs bundle exec swarf
22
- ```
23
-
24
- Named paths are always scored, even under `test/`, `db/` or a `.swarfignore` pattern.
25
-
26
- 3. Take the top rows and act on the Evidence column (next section). Write or extend the test, re-run the suite, score again.
27
-
28
- 4. Stop when the touched methods sit at `CRAP = CC` with `Cov% 100.0`. Nothing you test pulls a score under its CC. If a score is too high at full coverage, that is complexity, which is a refactor question, not a testing one. Do not split methods to chase the number.
29
-
30
- Before saying a change is done, run step 2 and report the top rows.
31
-
32
- ## Reading a row
33
-
34
- ```
35
- Method CC Cov% CRAP Evidence Location
36
- Cart#checkout 6 0.0% 42.00 never called lib/cart.rb:31
37
- Cart#discount 4 50.0% 6.00 3/6 br lib/cart.rb:24
38
- ```
39
-
40
- | Evidence | Meaning | Do |
41
- | -------------- | -------------------------------------------- | ------------------------------------------------------------- |
42
- | `never called` | A VM call count of zero. No test reaches it. | Write a test that calls it. |
43
- | `3/6 br` | 3 of 6 branch outcomes ran. | Extend a test to the untaken outcomes. |
44
- | `1/1 ln` | No branches, so lines are the whole truth. | Nothing, at 100%. |
45
- | `no data` | No run has been recorded for this file. | The probe is not loaded, or the suite has not run. Fix Setup. |
46
- | `stale` | The file changed after it was measured. | Re-run the suite, then score again. |
47
-
48
- `no data` and `stale` both score at the `CC² + CC` floor rather than guess. They are also counted per file under the table, because the fix is per file.
49
-
50
- A `never called` on a method a test clearly exercises means the test ran without the probe, or the probe loaded after the file. Check Setup before writing a duplicate test.
51
-
52
- ## Setup
53
-
54
- The probe must load before the code under test, because `Coverage` only sees files loaded after it starts. One line, placed first:
55
-
56
- - **RSpec**: first line of `.rspec`: `--require swarf/probe`
57
- - **Minitest**: first line of `test/test_helper.rb`: `require "swarf/probe"`
58
- - **Rails**: in `test/test_helper.rb`, after `require "bundler/setup"` and before `require_relative "../config/environment"`. Below the environment require, the whole app is invisible to it.
59
- - **Anything**: `RUBYOPT="-rswarf/probe" bundle exec rake test`
60
-
61
- Add `.swarf/` to `.gitignore`. Runs merge, so a single spec file adds to what earlier runs proved; nothing is erased until the file's bytes change.
62
-
63
- Do not run swarf's probe alongside SimpleCov. Ruby allows one `Coverage.start` per process; if SimpleCov started first, swarf warns and records nothing.
64
-
65
- ## Commands
66
-
67
- ```
68
- bundle exec swarf # whole project, worst 20
69
- bundle exec swarf lib/ app/ # directories
70
- bundle exec swarf app/models/cart.rb # one file
71
- bundle exec swarf --limit 0 # every row
72
- bundle exec swarf --ignore "app/legacy/**"
73
- bundle exec swarf --all # ignore nothing, including db/ and .swarfignore
74
- SWARF_DIR=/tmp/swarf bundle exec swarf # store elsewhere; set for the test run too
75
- ```
76
-
77
- swarf's CC counts `if`, `unless`, `while`, `until`, `for`, `when`, `in`, `rescue`, `&&`, `||` and `&.`, not blocks, so it will not match RuboCop's. Methods made by `define_method` are not seen.
data/.gitignore DELETED
@@ -1,10 +0,0 @@
1
- /.bundle/
2
- /.yardoc
3
- /_yardoc/
4
- /coverage/
5
- /doc/
6
- /pkg/
7
- /spec/reports/
8
- /tmp/
9
- /test/fixtures/
10
- .swarf/
data/CLAUDE.md DELETED
@@ -1,31 +0,0 @@
1
- # Epok
2
-
3
- Ruby gem wrapping the GCBA EPOk API (https://epok.buenosaires.gob.ar).
4
- No runtime dependencies beyond the standard library.
5
-
6
- ## Development
7
-
8
- ```
9
- bundle install
10
- bundle exec rake test # records VCR cassettes into test/fixtures on first run
11
- bundle exec swarf lib/ # complexity vs. coverage report, worst first
12
- ```
13
-
14
- Cassettes are gitignored, so the first test run hits the live API.
15
- The API rejects curl's default User-Agent, but Ruby's Net::HTTP works fine.
16
-
17
- ## Releasing
18
-
19
- 1. Make sure `main` is clean and pushed, and `bundle exec rake test` passes.
20
- 2. Bump `Epok::VERSION` in `lib/epok/version.rb`. Follow semver:
21
- patch for fixes, minor for new behaviour, major for breaking changes.
22
- 3. Commit it as `Bump version to X.Y.Z`.
23
- 4. Run `bundle exec rake release`. This builds the gem, creates and pushes
24
- the `vX.Y.Z` tag, and pushes the gem to rubygems.org. It needs rubygems
25
- credentials (`gem signin`) and may prompt for an OTP if the account has
26
- MFA enabled. If it prompts, the user has to run it themselves.
27
- 5. Create a GitHub release from the tag with a short summary of what changed:
28
- `gh release create vX.Y.Z --generate-notes`.
29
-
30
- Version numbers on rubygems.org cannot be reused, so check the diff before
31
- running step 4.
data/CODE_OF_CONDUCT.md DELETED
@@ -1,74 +0,0 @@
1
- # Contributor Covenant Code of Conduct
2
-
3
- ## Our Pledge
4
-
5
- In the interest of fostering an open and welcoming environment, we as
6
- contributors and maintainers pledge to making participation in our project and
7
- our community a harassment-free experience for everyone, regardless of age, body
8
- size, disability, ethnicity, gender identity and expression, level of experience,
9
- nationality, personal appearance, race, religion, or sexual identity and
10
- orientation.
11
-
12
- ## Our Standards
13
-
14
- Examples of behavior that contributes to creating a positive environment
15
- include:
16
-
17
- * Using welcoming and inclusive language
18
- * Being respectful of differing viewpoints and experiences
19
- * Gracefully accepting constructive criticism
20
- * Focusing on what is best for the community
21
- * Showing empathy towards other community members
22
-
23
- Examples of unacceptable behavior by participants include:
24
-
25
- * The use of sexualized language or imagery and unwelcome sexual attention or
26
- advances
27
- * Trolling, insulting/derogatory comments, and personal or political attacks
28
- * Public or private harassment
29
- * Publishing others' private information, such as a physical or electronic
30
- address, without explicit permission
31
- * Other conduct which could reasonably be considered inappropriate in a
32
- professional setting
33
-
34
- ## Our Responsibilities
35
-
36
- Project maintainers are responsible for clarifying the standards of acceptable
37
- behavior and are expected to take appropriate and fair corrective action in
38
- response to any instances of unacceptable behavior.
39
-
40
- Project maintainers have the right and responsibility to remove, edit, or
41
- reject comments, commits, code, wiki edits, issues, and other contributions
42
- that are not aligned to this Code of Conduct, or to ban temporarily or
43
- permanently any contributor for other behaviors that they deem inappropriate,
44
- threatening, offensive, or harmful.
45
-
46
- ## Scope
47
-
48
- This Code of Conduct applies both within project spaces and in public spaces
49
- when an individual is representing the project or its community. Examples of
50
- representing a project or community include using an official project e-mail
51
- address, posting via an official social media account, or acting as an appointed
52
- representative at an online or offline event. Representation of a project may be
53
- further defined and clarified by project maintainers.
54
-
55
- ## Enforcement
56
-
57
- Instances of abusive, harassing, or otherwise unacceptable behavior may be
58
- reported by contacting the project team at arzezak@gmail.com. All
59
- complaints will be reviewed and investigated and will result in a response that
60
- is deemed necessary and appropriate to the circumstances. The project team is
61
- obligated to maintain confidentiality with regard to the reporter of an incident.
62
- Further details of specific enforcement policies may be posted separately.
63
-
64
- Project maintainers who do not follow or enforce the Code of Conduct in good
65
- faith may face temporary or permanent repercussions as determined by other
66
- members of the project's leadership.
67
-
68
- ## Attribution
69
-
70
- This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71
- available at [http://contributor-covenant.org/version/1/4][version]
72
-
73
- [homepage]: http://contributor-covenant.org
74
- [version]: http://contributor-covenant.org/version/1/4/
data/Gemfile DELETED
@@ -1,7 +0,0 @@
1
- source "https://rubygems.org"
2
-
3
- git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
4
-
5
- gemspec
6
-
7
- gem "swarf", group: :development
data/Gemfile.lock DELETED
@@ -1,67 +0,0 @@
1
- PATH
2
- remote: .
3
- specs:
4
- epok (0.2.0)
5
-
6
- GEM
7
- remote: https://rubygems.org/
8
- specs:
9
- addressable (2.9.0)
10
- public_suffix (>= 2.0.2, < 8.0)
11
- ansi (1.6.0)
12
- bigdecimal (4.1.3)
13
- builder (3.3.0)
14
- crack (1.0.1)
15
- bigdecimal
16
- rexml
17
- hashdiff (1.2.1)
18
- minitest (5.27.0)
19
- minitest-reporters (1.8.0)
20
- ansi
21
- builder
22
- minitest (>= 5.0, < 7)
23
- ruby-progressbar
24
- public_suffix (7.0.5)
25
- rake (13.4.2)
26
- rexml (3.4.4)
27
- ruby-progressbar (1.13.0)
28
- swarf (0.2.0)
29
- vcr (6.4.0)
30
- webmock (3.26.4)
31
- addressable (>= 2.8.0)
32
- crack (>= 0.3.2)
33
- hashdiff (>= 0.4.0, < 2.0.0)
34
-
35
- PLATFORMS
36
- arm64-darwin-23
37
- ruby
38
-
39
- DEPENDENCIES
40
- epok!
41
- minitest (~> 5.0)
42
- minitest-reporters (~> 1.3, >= 1.3.6)
43
- rake (~> 13.0)
44
- swarf
45
- vcr (~> 6.0)
46
- webmock (~> 3.5, >= 3.5.1)
47
-
48
- CHECKSUMS
49
- addressable (2.9.0) sha256=7fdf6ac3660f7f4e867a0838be3f6cf722ace541dd97767fa42bc6cfa980c7af
50
- ansi (1.6.0) sha256=ac9ea0c0ea8d32fb4e271348e609963ac78882f34b73836c2a02b3622e666658
51
- bigdecimal (4.1.3) sha256=61ebe1e5e559bdc3cc6f2c0ee7f427321fc838f59611c294356eb04d6e21cf66
52
- builder (3.3.0) sha256=497918d2f9dca528fdca4b88d84e4ef4387256d984b8154e9d5d3fe5a9c8835f
53
- crack (1.0.1) sha256=ff4a10390cd31d66440b7524eb1841874db86201d5b70032028553130b6d4c7e
54
- epok (0.2.0)
55
- hashdiff (1.2.1) sha256=9c079dbc513dfc8833ab59c0c2d8f230fa28499cc5efb4b8dd276cf931457cd1
56
- minitest (5.27.0) sha256=2d3b17f8a36fe7801c1adcffdbc38233b938eb0b4966e97a6739055a45fa77d5
57
- minitest-reporters (1.8.0) sha256=8ce5280fb73ad3178ae525454df169b6f28c1b38b1d088ea91815d3a370ba384
58
- public_suffix (7.0.5) sha256=1a8bb08f1bbea19228d3bed6e5ed908d1cb4f7c2726d18bd9cadf60bc676f623
59
- rake (13.4.2) sha256=cb825b2bd5f1f8e91ca37bddb4b9aaf345551b4731da62949be002fa89283701
60
- rexml (3.4.4) sha256=19e0a2c3425dfbf2d4fc1189747bdb2f849b6c5e74180401b15734bc97b5d142
61
- ruby-progressbar (1.13.0) sha256=80fc9c47a9b640d6834e0dc7b3c94c9df37f08cb072b7761e4a71e22cff29b33
62
- swarf (0.2.0) sha256=b4306a244fe4b096cfe263e452e666156856eac2467ae5d770639ef11388b375
63
- vcr (6.4.0) sha256=077ac92cc16efc5904eb90492a18153b5e6ca5398046d8a249a7c96a9ea24ae6
64
- webmock (3.26.4) sha256=8d8da206d217ebe6968cfb09c77f4533c23074e1432bad865f3994eacbaad50d
65
-
66
- BUNDLED WITH
67
- 4.0.20
data/Rakefile DELETED
@@ -1,10 +0,0 @@
1
- require "bundler/gem_tasks"
2
- require "rake/testtask"
3
-
4
- Rake::TestTask.new(:test) do |t|
5
- t.libs << "test"
6
- t.libs << "lib"
7
- t.test_files = FileList["test/**/*_test.rb"]
8
- end
9
-
10
- task default: :test
data/bin/console DELETED
@@ -1,9 +0,0 @@
1
- #!/usr/bin/env ruby
2
-
3
- require "bundler/setup"
4
- require "epok"
5
- require "irb"
6
-
7
- @obelisco = Epok::Location.new(x: -58.381570, y: -34.603738)
8
-
9
- IRB.start(__FILE__)
data/bin/setup DELETED
@@ -1,7 +0,0 @@
1
- #!/usr/bin/env bash
2
-
3
- set -euo pipefail
4
- IFS=$'\n\t'
5
- set -vx
6
-
7
- bundle install
data/epok.gemspec DELETED
@@ -1,25 +0,0 @@
1
- lib = File.expand_path("lib", __dir__)
2
- $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
-
4
- require "epok/version"
5
-
6
- Gem::Specification.new do |spec|
7
- spec.name = "epok"
8
- spec.version = Epok::VERSION
9
- spec.authors = ["Ariel Rzezak"]
10
- spec.email = ["arzezak@gmail.com"]
11
-
12
- spec.summary = "GCBA EPOk wrapper."
13
- spec.description = "GCBA EPOk wrapper to query indexed geographic objects."
14
- spec.homepage = "https://github.com/arzezak/epok"
15
- spec.license = "MIT"
16
-
17
- spec.files = `git ls-files`.split("\n")
18
- spec.require_paths = ["lib"]
19
-
20
- spec.add_development_dependency "minitest", "~> 5.0"
21
- spec.add_development_dependency "minitest-reporters", "~> 1.3", ">= 1.3.6"
22
- spec.add_development_dependency "rake", "~> 13.0"
23
- spec.add_development_dependency "vcr", "~> 6.0"
24
- spec.add_development_dependency "webmock", "~> 3.5", ">= 3.5.1"
25
- end
@@ -1,17 +0,0 @@
1
- require "forwardable"
2
-
3
- module Epok
4
- module Collectable
5
- def self.included(base)
6
- base.class_eval do
7
- extend Forwardable
8
-
9
- def_delegators :results, :entries, :first
10
- end
11
- end
12
-
13
- def collection(object)
14
- Collection.new object
15
- end
16
- end
17
- end
data/lib/epok/geocoder.rb DELETED
@@ -1,21 +0,0 @@
1
- module Epok
2
- Location = Struct.new(:x, :y, keyword_init: true)
3
-
4
- class Geocoder
5
- include Collectable
6
-
7
- attr_reader :x, :y, :categories
8
-
9
- def initialize(location, categories)
10
- @x = location.x
11
- @y = location.y
12
- @categories = categories
13
- end
14
-
15
- private
16
-
17
- def results
18
- collection API.geocoder(x, y, categories)
19
- end
20
- end
21
- end
data/lib/epok/search.rb DELETED
@@ -1,17 +0,0 @@
1
- module Epok
2
- class Search
3
- include Collectable
4
-
5
- attr_reader :query
6
-
7
- def initialize(query)
8
- @query = query
9
- end
10
-
11
- private
12
-
13
- def results
14
- collection API.search(query)
15
- end
16
- end
17
- end
@@ -1,29 +0,0 @@
1
- require "test_helper"
2
-
3
- module Epok
4
- class GeocoderTest < Minitest::Test
5
- def setup
6
- VCR.insert_cassette("geocoder", record: :new_episodes)
7
- end
8
-
9
- def teardown
10
- VCR.eject_cassette("geocoder")
11
- end
12
-
13
- def test_that_geocoder_results_are_epok_objects
14
- assert_instance_of Object, result
15
- end
16
-
17
- def test_that_a_single_geocoder_result_has_a_name
18
- assert_match "Línea D", result.name
19
- end
20
-
21
- private
22
-
23
- def result
24
- obelisco = Location.new(x: -58.381570, y: -34.603738)
25
- results = Geocoder.new(obelisco, "estaciones_de_subte")
26
- results.first
27
- end
28
- end
29
- end
@@ -1,45 +0,0 @@
1
- require "test_helper"
2
-
3
- module Epok
4
- class ObjectTest < Minitest::Test
5
- def setup
6
- VCR.insert_cassette("object")
7
- end
8
-
9
- def teardown
10
- VCR.eject_cassette("object")
11
- end
12
-
13
- def test_that_an_object_has_a_name
14
- assert_equal "BIBLIOTECA ANMAT", object.name
15
- end
16
-
17
- def test_that_an_object_has_a_normalized_address
18
- assert_equal "DE MAYO AV. 869", object.normalized_address
19
- end
20
-
21
- def test_that_an_object_has_content
22
- expected_content = {
23
- "Nombre" => "BIBLIOTECA ANMAT",
24
- "Categoria" => "BIBLIOTECA",
25
- "Subcategoria" => "ESPECIALIZADA GUBERNAMENTAL",
26
- "Teléfonos" => "54.011 4340-0800 INT. 1047 FAX 54.011 4340-0800 INT. 1048",
27
- "E-Mail" => "<a href=\"mailto:NDURAND@ANMAT.GOV.AR\">NDURAND@ANMAT.GOV.AR</a>",
28
- "Página WEB" => "<a href=\"http://WWW.ANMAT.GOV.AR\" target=\"_blank\">WWW.ANMAT.GOV.AR</a>",
29
- "Dependencia" => "ADMINISTRACION NACIONAL DE MEDICAMENTOS Y TECNOLOGIA MEDICA ANMAT",
30
- "Sector" => "PUBLICO",
31
- "Dirección" => "DE MAYO AV. 869",
32
- "Barrio" => "MONSERRAT",
33
- "Comuna" => "Comuna 1"
34
- }
35
-
36
- assert_equal expected_content, object.content
37
- end
38
-
39
- private
40
-
41
- def object
42
- Object.new("dependencias_culturales|1635")
43
- end
44
- end
45
- end
@@ -1,28 +0,0 @@
1
- require "test_helper"
2
-
3
- module Epok
4
- class SearchTest < Minitest::Test
5
- def setup
6
- VCR.insert_cassette("search")
7
- end
8
-
9
- def teardown
10
- VCR.eject_cassette("search")
11
- end
12
-
13
- def test_that_search_results_are_epok_objects
14
- assert_instance_of Object, result
15
- end
16
-
17
- def test_that_a_single_search_result_has_a_name
18
- assert_match "San Miguel de Garicoits", result.name
19
- end
20
-
21
- private
22
-
23
- def result
24
- results = Search.new("garicoits")
25
- results.first
26
- end
27
- end
28
- end
data/test/epok_test.rb DELETED
@@ -1,20 +0,0 @@
1
- require "test_helper"
2
-
3
- class EpokTest < Minitest::Test
4
- def test_that_search_builds_a_search
5
- search = Epok.search("garicoits")
6
-
7
- assert_instance_of Epok::Search, search
8
- assert_equal "garicoits", search.query
9
- end
10
-
11
- def test_that_geocoder_builds_a_geocoder
12
- obelisco = Epok::Location.new(x: -58.381570, y: -34.603738)
13
- geocoder = Epok.geocoder(obelisco, "farmacias")
14
-
15
- assert_instance_of Epok::Geocoder, geocoder
16
- assert_equal(-58.381570, geocoder.x)
17
- assert_equal(-34.603738, geocoder.y)
18
- assert_equal "farmacias", geocoder.categories
19
- end
20
- end
data/test/test_helper.rb DELETED
@@ -1,15 +0,0 @@
1
- require "swarf/probe"
2
-
3
- $LOAD_PATH.unshift File.expand_path("../lib", __dir__)
4
-
5
- require "epok"
6
- require "minitest/autorun"
7
- require "minitest/reporters"
8
- require "vcr"
9
-
10
- Minitest::Reporters.use!
11
-
12
- VCR.configure do |config|
13
- config.cassette_library_dir = "test/fixtures"
14
- config.hook_into :webmock
15
- end