physical 0.5.1 → 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: 27de598d29f3754f4a2f4676208da48ba8d83b4e59662c5bd267dffb3ccdee58
4
- data.tar.gz: 38cc5da575f4e56cf001370aaea5fd906b82373f75fa6880d804fa20e1b828b9
3
+ metadata.gz: 6ff0a2d8dd4662fd4642901ea536db815d6d51f318a2f6d8a66da1d32e5f7555
4
+ data.tar.gz: 991d0b406e3cadd1188fff3d84205467d9e47e93e81bfe4d3b6aa9db32e5f9a5
5
5
  SHA512:
6
- metadata.gz: 9f80884f5e7333d29be10b70e27e683865e0a0ac1f43cfbb464d13008a482ea57932ccbd963599f3412563b556d535de54c43ab21ffb7b8ad0bac2d859895e26
7
- data.tar.gz: abfb28ea0aa1fa6fdcfb8e4b31e7b5412d743a45edc4c22cd71d1b8146cdac741b5da69f362a573ac34985628e208e92c8c442a7de7aaf4e615f4ea6398e404e
6
+ metadata.gz: 15b3404acebb3c0d24d396cbcb66205c0d457148c5a1a5b08e2bf920a35d7e8e1edddf246707772337e08dc75c448dd3ec0bac0d49aeda26ef6a3a5ea07f92d2
7
+ data.tar.gz: 30e61b6929b17288830eee2e68967970895807c3d8695f189dd52fb31c783925245d2b6733d1e785fdef8800a1265fd9b316b1f23005e13c55d6bf59d6157056
data/.yardopts ADDED
@@ -0,0 +1,9 @@
1
+ --protected
2
+ --no-private
3
+ --readme README.md
4
+ --markup markdown
5
+ --exclude test_support
6
+ --exclude measured/density
7
+ --exclude physical/types
8
+ --exclude physical/property_readers
9
+ 'lib/**/*.rb' - '*.md'
data/CHANGELOG.md CHANGED
@@ -6,6 +6,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
6
6
 
7
7
  ## Unreleased
8
8
 
9
+ ## [0.6.0] - 2025-08-14
10
+
11
+ ### Changed
12
+ - Require Ruby >= 3.0
13
+ - Upgrade `measured` dependency to `~> 3.0` (bundled as 3.2.1)
14
+
9
15
  ## [0.5.1] - 2023-12-19
10
16
 
11
17
  ### Changed
data/README.md CHANGED
@@ -1,20 +1,38 @@
1
1
  # Physical
2
2
 
3
- This is a small library to describe Packages (that could, potentially, be mailed).
3
+ This is a small library to describe packages (that could, potentially, be mailed) and
4
+ structures that store packages.
4
5
 
5
- A `Physical::Package` represents a package. It has and ID, dimensions, and a weight.
6
+ ### Packages
6
7
 
7
- The `Package` takes it's dimensions from its `container` object, usually a `Physical::Box`.
8
- It also has a `Set` of `Physical::Item`s that together make with the container make up everything
9
- about the Package.
8
+ A `Physical::Package` represents a package. It has an ID, dimensions, and weight.
10
9
 
11
- All things are thought to be cuboid: They have three dimensions, and a weight.
10
+ The `Package` takes its dimensions from its `container` object, usually a `Physical::Box`.
11
+ It also has a `Set` of `Physical::Item`s that together with the container make up everything
12
+ about the package.
12
13
 
13
- The weight of a `Package` is the weight of it's container plus the weight of all items, plus the weight
14
- of any void fill.
14
+ All containers are thought to be `Cuboid`: they have three dimensions and a weight.
15
15
 
16
- By default, the `Physical::Box` is infinitely large. In order to limit the size of the Box, simply give it
17
- dimensions.
16
+ The weight of a `Package` is the weight of its container plus the weight of all items plus
17
+ the weight of any void fill.
18
+
19
+ By default, the `Physical::Box` container is infinitely large. In order to limit the size
20
+ of the container, simply give it dimensions.
21
+
22
+ ### Structures
23
+
24
+ A `Physical::Structure` represents a pallet, skid, rack, or some other collection of packages.
25
+ Similar to a `Package`, it has an ID, dimensions, and weight.
26
+
27
+ The `Structure` takes its dimensions from its `container` object, usually a `Physical::Pallet`.
28
+ It also has an `Array` of `Physical::Package`s that together with the container make up
29
+ everything about the structure.
30
+
31
+ The weight of a `Structure` is the weight of its container plus the weight of all packages.
32
+ Structures do not have void fill.
33
+
34
+ By default, the `Physical::Pallet` container is infinitely large. In order to limit the size
35
+ of the container, simply give it dimensions.
18
36
 
19
37
  ## Installation
20
38
 
@@ -34,9 +52,11 @@ Or install it yourself as:
34
52
 
35
53
  ## Usage
36
54
 
37
- ### Basic Package
55
+ ### Basic package
38
56
 
39
- A basic Package has no items and is simply assigned a weight and dimensions. Weights and Dimensions have to be given as `Measured::Weight` or as an Array of `Measured::Length` objects:
57
+ A basic package has no items and is simply assigned a weight and dimensions. Weights must
58
+ be specified as `Measured::Weight` objects. Dimensions must be specified as an array of
59
+ `Measured::Length` objects.
40
60
 
41
61
  ```ruby
42
62
  Physical::Package.new(
@@ -48,29 +68,34 @@ Physical::Package.new(
48
68
  ]
49
69
  )
50
70
  ```
51
- You will be able to retrieve the package's weight and dimensions using the `#weight` and `#dimensions` attribute reader methods.
52
-
53
- ### Convenience methods
71
+ The package's weight and dimensions are retrieved using the `#weight` and `#dimensions`
72
+ attribute reader methods.
54
73
 
55
- The length, width and height of a package are defined as the dimension array's first, second, and third argument, respectively. For the package from the previous example, `package.length` will be 3 inches, `package.width` will be 4 inches, and `package.height` will be 5 inches.
74
+ ### Package dimensions
56
75
 
57
- ### Packages with Items
76
+ The length, width and height of a package are defined as the dimension array's first,
77
+ second, and third argument, respectively. For the package from the previous example,
78
+ `#length` will be 3 inches, `#width` will be 4 inches, and `#height` will be 5 inches.
58
79
 
59
- The following example is a somewhat more elaborate package: We know the items inside! `Physical::Item` objects are Cuboids, so they have three dimensions and a weight. They also have a `properties` hash that can hold things like any hazardous properties that might impede shipping.
80
+ ### Packages with items
60
81
 
82
+ The following example is a somewhat more elaborate package: we know the items inside!
83
+ `Physical::Item` objects are `Cuboid`, so they have three dimensions and a weight. They also
84
+ have a `properties` hash that can hold things like any hazardous properties that might
85
+ impede shipping.
61
86
 
62
87
  ```ruby
63
- sku_one = Physical::Item.new(
88
+ item_one = Physical::Item.new(
64
89
  id: '12345',
65
90
  dimensions: [
66
91
  Measured::Length(2, :inch),
67
92
  Measured::Length(4, :inch),
68
93
  Measured::Length(5, :inch)
69
94
  ],
70
- weight: Measured::Weight(1, :kg),
95
+ weight: Measured::Weight(5, :g),
71
96
  )
72
97
 
73
- sku_two = Physical::Item.new(
98
+ item_two = Physical::Item.new(
74
99
  id: "54321",
75
100
  dimensions: [
76
101
  Measured::Length(1, :cm),
@@ -85,18 +110,22 @@ You can initialize a package with items as follows:
85
110
 
86
111
  ```ruby
87
112
  package_with_items = Physical::Package.new(
88
- items: [sku_one, sku_two]
113
+ items: [item_one, item_two]
89
114
  )
90
115
  ```
91
116
 
92
- This package has no defined container. This means we assume a box that is infinitely large, and that has zero weight. Thus the weight of this `package_with_items` will be 1023 gram (1 kg + 23 g = 1000 g + 23 g).
117
+ This package has no defined container. This means we assume a box that is infinitely large, and
118
+ that has zero weight. Thus the weight of this `package_with_items` will be 28 grams
119
+ (5 g + 23 g = 28 g).
93
120
 
94
- ### Packages with Boxes
121
+ ### Packages with boxes
95
122
 
96
- A package also has a box that wraps it. This box is assumed to be a Cuboid, too - but one that has inner dimensions, and a weight that is it's own weight which must be added to any item's weight in order to find out the total weight of a package.
123
+ A package also has a container that wraps it. This container is assumed to be a `Cuboid`, too -
124
+ but one that has inner dimensions, and a weight that is it's own weight which must be added to
125
+ item weights in order to find out the total weight of a package.
97
126
 
98
127
  ```ruby
99
- my_carton = Physical::Box.new(
128
+ container = Physical::Box.new(
100
129
  dimensions: [
101
130
  Measured::Length(10, :cm),
102
131
  Measured::Length(15, :cm),
@@ -111,22 +140,29 @@ my_carton = Physical::Box.new(
111
140
  )
112
141
  ```
113
142
 
114
- If you create a carton and omit the inner dimensions, we will assume that the carton's inner dimensions are equal to its outer dimensions. This will, in many cases, be good enough (but in some you'll need the extra precision).
143
+ If you create a container and omit the inner dimensions, we will assume that the container's inner
144
+ dimensions are equal to its outer dimensions. This will, in many cases, be good enough (but in some
145
+ cases you'll need the extra precision).
115
146
 
116
- ### Calculating Void Fill
147
+ ### Calculating void fill
117
148
 
118
- For an elaborate package with a container box and items, we still cannot find out the full weight of the package without taking into account void fill (styrofoam, bubble wrap or crumpled newspaper maybe). We can instruct the package to fill up all the volume not used up by items with void fill. You can pass the density as a `Measured::Weight` object that refers to the weight of 1 cubic centimeter of void fill:
149
+ For an elaborate package with a container box and items, we still cannot find out the full weight
150
+ of the package without taking into account void fill (styrofoam, bubble wrap or crumpled newspaper
151
+ maybe). We can instruct the package to fill up all the volume not used up by items with void fill.
152
+ You can pass the density as a `Measured::Weight` object that refers to the weight of 1 cubic
153
+ centimeter of void fill:
119
154
 
120
155
  ```ruby
121
156
  package = Physical::Package.new(
122
157
  id: "my_package",
123
- container: my_carton,
124
- items: [sku_one, sku_two],
158
+ container: container,
159
+ items: [item_one, item_two],
125
160
  void_fill_density: Measured::Weight(0.007, :g)
126
161
  )
127
162
  ```
128
163
 
129
- In this case, the package's weight will be slightly above the sum of carton weight and the sum of item weights, as we incorporate the approximate weight of the void fill material:
164
+ In this case, the package's weight will be slightly above the sum of container weight and the
165
+ sum of item weights, as we incorporate the approximate weight of the void fill material:
130
166
 
131
167
  ```ruby
132
168
  package.weight
@@ -135,15 +171,115 @@ package.weight
135
171
  package.remaining_volume
136
172
  => #<Measured::Volume: 1107.51744 #<Measured::Unit: ml (milliliter, millilitre, milliliters, millilitres) 1/1000 l>>
137
173
  ```
174
+ ### Basic structure
175
+
176
+ A basic structure has no packages and is simply assigned a weight and dimensions. Weights must
177
+ be specified as `Measured::Weight` objects. Dimensions must be specified as an array of
178
+ `Measured::Length` objects.
179
+
180
+ ```ruby
181
+ Physical::Structure.new(
182
+ weight: Measured::Weight(1, :pound),
183
+ dimensions: [
184
+ Measured::Length(48, :inch),
185
+ Measured::Length(48, :inch),
186
+ Measured::Length(96, :inch)
187
+ ]
188
+ )
189
+ ```
190
+ The structure's weight and dimensions are retrieved using the `#weight` and `#dimensions`
191
+ attribute reader methods.
192
+
193
+ ### Structure dimensions
194
+
195
+ The length, width and height of a structure are defined as the dimension array's first,
196
+ second, and third argument, respectively. For the structure from the previous example,
197
+ `#length` will be 48 inches, `#width` will be 48 inches, and `#height` will be 96 inches
198
+ (the approximate dimensions of a pallet).
199
+
200
+ ### Structures with packages
201
+
202
+ The following example is a somewhat more elaborate structure: we know the packages inside!
203
+ `Physical::Package` objects are `Cuboid`, so they have three dimensions and a weight. They also
204
+ have a `properties` hash that can hold things like any hazardous properties that might
205
+ impede shipping.
206
+
207
+ ```ruby
208
+ package_one = Physical::Package.new(
209
+ id: '12345',
210
+ dimensions: [
211
+ Measured::Length(2, :inch),
212
+ Measured::Length(4, :inch),
213
+ Measured::Length(5, :inch)
214
+ ],
215
+ weight: Measured::Weight(1, :kg),
216
+ )
217
+
218
+ package_two = Physical::Package.new(
219
+ id: "54321",
220
+ dimensions: [
221
+ Measured::Length(1, :cm),
222
+ Measured::Length(1, :cm),
223
+ Measured::Length(1, :cm)
224
+ ],
225
+ weight: Measured::Weight(23, :g)
226
+ )
227
+ ```
228
+
229
+ You can initialize a structure with packages as follows:
230
+
231
+ ```ruby
232
+ structure_with_packages = Physical::Structure.new(
233
+ items: [package_one, package_two]
234
+ )
235
+ ```
236
+
237
+ This structure has no defined container. This means we assume a pallet that is infinitely large,
238
+ and that has zero weight. Thus the weight of this `structure_with_packages` will be 1023 grams
239
+ (1 kg + 73 g = 1000 g + 23 g = 1023 g).
240
+
241
+ ### Structures with pallets
242
+
243
+ A structure also has a container that wraps it. This container is assumed to be a `Cuboid`, too -
244
+ but one that has inner dimensions, and a weight that is it's own weight which must be added to
245
+ package weights in order to find out the total weight of a structure.
246
+
247
+ ```ruby
248
+ container = Physical::Pallet.new(
249
+ dimensions: [
250
+ Measured::Length(10, :cm),
251
+ Measured::Length(15, :cm),
252
+ Measured::Length(15, :cm)
253
+ ],
254
+ inner_dimensions: [
255
+ Measured::Length(9, :cm),
256
+ Measured::Length(14, :cm),
257
+ Measured::Length(14, :cm)
258
+ ],
259
+ weight: Measured::Weight(350, :g),
260
+ )
261
+ ```
262
+
263
+ If you create a container and omit the inner dimensions, we will assume that the container's inner
264
+ dimensions are equal to its outer dimensions. This will, in many cases, be good enough (but in some
265
+ cases you'll need the extra precision).
266
+
138
267
  ## Development
139
268
 
140
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
269
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to
270
+ run the tests. You can also run `bin/console` for an interactive prompt that will allow you to
271
+ experiment.
141
272
 
142
- To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
273
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new
274
+ version, update the version number in `version.rb`, and then run `bundle exec rake release`,
275
+ which will create a git tag for the version, push git commits and tags, and push the `.gem`
276
+ file to [rubygems.org](https://rubygems.org).
143
277
 
144
278
  ## Contributing
145
279
 
146
- Bug reports and pull requests are welcome on GitHub at https://github.com/friendlycart/physical. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
280
+ Bug reports and pull requests are welcome on GitHub at https://github.com/friendlycart/physical.
281
+ This project is intended to be a safe, welcoming space for collaboration, and contributors are
282
+ expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
147
283
 
148
284
  ## License
149
285
 
@@ -151,4 +287,5 @@ The gem is available as open source under the terms of the [MIT License](https:/
151
287
 
152
288
  ## Code of Conduct
153
289
 
154
- Everyone interacting in the Physical project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/[USERNAME]/physical/blob/master/CODE_OF_CONDUCT.md).
290
+ Everyone interacting in the Physical project’s codebases, issue trackers, chat rooms and
291
+ mailing lists is expected to follow the [code of conduct](https://github.com/friendlycart/physical/blob/main/CODE_OF_CONDUCT.md).
data/lib/physical/box.rb CHANGED
@@ -3,25 +3,52 @@
3
3
  require 'measured'
4
4
 
5
5
  module Physical
6
+ # Represents a physical box which items can be packed into.
6
7
  class Box < Cuboid
8
+ # The default dimensions of this box when unspecified
7
9
  DEFAULT_LENGTH = BigDecimal::INFINITY
10
+
11
+ # The default maximum weight of this box when unspecified
8
12
  DEFAULT_MAX_WEIGHT = BigDecimal::INFINITY
9
13
 
10
- attr_reader :inner_dimensions,
11
- :inner_length,
12
- :inner_width,
13
- :inner_height,
14
- :max_weight
14
+ # The inner length, width, and height of this box
15
+ # @return [Array<Measured::Length>]
16
+ attr_reader :inner_dimensions
17
+
18
+ # The inner length of this box
19
+ # @return [Measured::Length]
20
+ attr_reader :inner_length
21
+
22
+ # The inner width of this box
23
+ # @return [Measured::Length]
24
+ attr_reader :inner_width
25
+
26
+ # The inner height of this box
27
+ # @return [Measured::Length]
28
+ attr_reader :inner_height
29
+
30
+ # The maximum weight this box can handle
31
+ # @return [Measured::Weight]
32
+ attr_reader :max_weight
15
33
 
16
- def initialize(**args)
17
- inner_dimensions = args.delete(:inner_dimensions) || []
18
- max_weight = args.delete(:max_weight) || Measured::Weight(DEFAULT_MAX_WEIGHT, :g)
19
- super(**args)
34
+ # @param [Hash] kwargs ID, dimensions, weight, and properties
35
+ # @option kwargs [String] :id a unique identifier for this box
36
+ # @option kwargs [Array<Measured::Length>] :dimensions the outer length, width, and height of this box
37
+ # @option kwargs [Array<Measured::Length>] :inner_dimensions the inner length, width, and height of this box
38
+ # @option kwargs [Measured::Weight] :weight the weight of the box itself (excluding what's inside)
39
+ # @option kwargs [Measured::Weight] :max_weight the maximum weight this box can handle
40
+ # @option kwargs [Hash] :properties additional custom properties for this box
41
+ def initialize(**kwargs)
42
+ inner_dimensions = kwargs.delete(:inner_dimensions) || []
43
+ max_weight = kwargs.delete(:max_weight) || Measured::Weight(DEFAULT_MAX_WEIGHT, :g)
44
+ super(**kwargs)
20
45
  @inner_dimensions = fill_dimensions(Types::Dimensions[inner_dimensions])
21
46
  @inner_length, @inner_width, @inner_height = *@inner_dimensions
22
47
  @max_weight = Types::Weight[max_weight]
23
48
  end
24
49
 
50
+ # Calculates and returns this box's volume based on the inner dimensions
51
+ # @return [Measured::Volume]
25
52
  def inner_volume
26
53
  Measured::Volume(
27
54
  inner_dimensions.map { |d| d.convert_to(:cm).value }.reduce(1, &:*),
@@ -29,6 +56,7 @@ module Physical
29
56
  )
30
57
  end
31
58
 
59
+ # Returns true if the given item can fit inside this box
32
60
  # @param [Physical::Item] item
33
61
  # @return [Boolean]
34
62
  def item_fits?(item)
@@ -3,11 +3,42 @@
3
3
  require 'measured'
4
4
 
5
5
  module Physical
6
+ # Represents a cube-shaped physical object with dimensions and weight.
6
7
  class Cuboid
7
8
  include PropertyReaders
8
9
 
9
- attr_reader :dimensions, :length, :width, :height, :weight, :id, :properties
10
+ # A unique identifier for this cuboid
11
+ # @return [String]
12
+ attr_reader :id
10
13
 
14
+ # The length, width, and height of this cuboid
15
+ # @return [Array<Measured::Length>]
16
+ attr_reader :dimensions
17
+
18
+ # The length of this cuboid
19
+ # @return <Measured::Length>
20
+ attr_reader :length
21
+
22
+ # The width of this cuboid
23
+ # @return <Measured::Length>
24
+ attr_reader :width
25
+
26
+ # The height of this cuboid
27
+ # @return <Measured::Length>
28
+ attr_reader :height
29
+
30
+ # The weight of the cuboid itself (excluding what's inside)
31
+ # @return [Measured::Weight]
32
+ attr_reader :weight
33
+
34
+ # Additional custom properties for this cuboid
35
+ # @return [Hash]
36
+ attr_reader :properties
37
+
38
+ # @param [String] id a unique identifier for this cuboid
39
+ # @param [Array<Measured::Length>] dimensions the length, width, and height of this cuboid
40
+ # @param [Measured::Weight] weight the weight of the cuboid itself (excluding what's inside)
41
+ # @param [Hash] properties additional custom properties for this cuboid
11
42
  def initialize(id: nil, dimensions: [], weight: Measured::Weight(0, :g), properties: {})
12
43
  @id = id || SecureRandom.uuid
13
44
  @weight = Types::Weight[weight]
@@ -17,10 +48,14 @@ module Physical
17
48
  @properties = properties
18
49
  end
19
50
 
51
+ # Calculates and returns this cuboid's volume based on its dimensions.
52
+ # @return [Measured::Volume]
20
53
  def volume
21
54
  Measured::Volume(dimensions.map { |d| d.convert_to(:cm).value }.reduce(1, &:*), :ml)
22
55
  end
23
56
 
57
+ # Calculates and returns this cuboid's density based on its volume and weight.
58
+ # @return [Measured::Density]
24
59
  def density
25
60
  return Measured::Density(Float::INFINITY, :g_ml) if volume.value.zero?
26
61
  return Measured::Density(0.0, :g_ml) if volume.value.infinite?
@@ -28,6 +63,9 @@ module Physical
28
63
  Measured::Density(weight.convert_to(:g).value / volume.convert_to(:ml).value, :g_ml)
29
64
  end
30
65
 
66
+ # Returns true if the given object shares the same class and ID with this cuboid.
67
+ # @param [Object] other
68
+ # @return [Boolean]
31
69
  def ==(other)
32
70
  other.is_a?(self.class) &&
33
71
  id == other&.id
@@ -35,6 +73,9 @@ module Physical
35
73
 
36
74
  private
37
75
 
76
+ # Fills an array with dimensions or with default values if unspecified.
77
+ # @param [Array<Measured::Length>] dimensions
78
+ # @return [Array<Measured::Length>]
38
79
  def fill_dimensions(dimensions)
39
80
  dimensions.fill(dimensions.length..2) do |index|
40
81
  @dimensions[index] || Measured::Length(self.class::DEFAULT_LENGTH, :cm)
data/lib/physical/item.rb CHANGED
@@ -1,13 +1,31 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Physical
4
+ # Represents a physical item which can be packed into a box.
4
5
  class Item < Cuboid
6
+ # The default dimensions of this item when unspecified
5
7
  DEFAULT_LENGTH = 0
6
8
 
7
- attr_reader :cost,
8
- :sku,
9
- :description
9
+ # The cost for this item
10
+ # @return [Money]
11
+ attr_reader :cost
10
12
 
13
+ # The SKU for this item
14
+ # @return [String]
15
+ attr_reader :sku
16
+
17
+ # A description for this item
18
+ # @return [String]
19
+ attr_reader :description
20
+
21
+ # @param [Hash] kwargs ID, dimensions, weight, and properties
22
+ # @option kwargs [String] :id a unique identifier for this item
23
+ # @option kwargs [Money] :cost the cost of this item
24
+ # @option kwargs [String] :sku the SKU for this item
25
+ # @option kwargs [String] :description a description for this item
26
+ # @option kwargs [Array<Measured::Length>] :dimensions the length, width, and height of this item
27
+ # @option kwargs [Measured::Weight] :weight the weight of this item
28
+ # @option kwargs [Hash] :properties additional custom properties for this item
11
29
  def initialize(**kwargs)
12
30
  @cost = Types::Money.optional[kwargs.delete(:cost)]
13
31
  @sku = kwargs.delete(:sku)
@@ -3,28 +3,93 @@
3
3
  require 'carmen'
4
4
 
5
5
  module Physical
6
+ # Represents a physical location.
6
7
  class Location
7
8
  include PropertyReaders
8
9
 
10
+ # Possible address types for this location
9
11
  ADDRESS_TYPES = %w(residential commercial po_box).freeze
10
12
 
11
- attr_reader :country,
12
- :zip,
13
- :region,
14
- :city,
15
- :name,
16
- :address1,
17
- :address2,
18
- :address3,
19
- :phone,
20
- :fax,
21
- :email,
22
- :address_type,
23
- :company_name,
24
- :latitude,
25
- :longitude,
26
- :properties
13
+ # This location's country
14
+ # @return [Carmen::Country]
15
+ attr_reader :country
27
16
 
17
+ # This location's postal code
18
+ # @return [String]
19
+ attr_reader :zip
20
+
21
+ # This location's state or province
22
+ # @return [Carmen::Region]
23
+ attr_reader :region
24
+
25
+ # This location's city
26
+ # @return [String]
27
+ attr_reader :city
28
+
29
+ # This location's name (could be a person's name)
30
+ # @return [String]
31
+ attr_reader :name
32
+
33
+ # This location's address line 1
34
+ # @return [String]
35
+ attr_reader :address1
36
+
37
+ # This location's address line 2
38
+ # @return [String]
39
+ attr_reader :address2
40
+
41
+ # This location's address line 3
42
+ # @return [String]
43
+ attr_reader :address3
44
+
45
+ # This location's phone
46
+ # @return [String]
47
+ attr_reader :phone
48
+
49
+ # This location's fax
50
+ # @return [String]
51
+ attr_reader :fax
52
+
53
+ # This location's email
54
+ # @return [String]
55
+ attr_reader :email
56
+
57
+ # This location's address type (see {ADDRESS_TYPES})
58
+ # @return [String]
59
+ attr_reader :address_type
60
+
61
+ # This location's company name
62
+ # @return [String]
63
+ attr_reader :company_name
64
+
65
+ # This location's latitude
66
+ # @return [String]
67
+ attr_reader :latitude
68
+
69
+ # This location's longitude
70
+ # @return [String]
71
+ attr_reader :longitude
72
+
73
+ # Additional custom properties for this location
74
+ # @return [String]
75
+ attr_reader :properties
76
+
77
+ # @param [String] name the name of this location (could be a person's name)
78
+ # @param [String] company_name the name of the company at this location
79
+ # @param [String] address1 the first line of the address
80
+ # @param [String] address2 the second line of the address
81
+ # @param [String] address3 the third line of the address
82
+ # @param [String] city the city
83
+ # @param [String, Carmen::Region] region the state or province
84
+ # @param [String] zip the postal code
85
+ # @param [String, Carmen::Country] country the country
86
+ # @param [String] phone the phone number
87
+ # @param [String] fax the fax number
88
+ # @param [String] email the email address
89
+ # @param [String] address_type the type of address (see {ADDRESS_TYPES})
90
+ # @param [String] latitude the latitude at this location
91
+ # @param [String] longitude the longitude at this location
92
+ # @param [Hash] properties additional custom properties for this location
28
93
  def initialize(
29
94
  name: nil,
30
95
  company_name: nil,
@@ -72,18 +137,26 @@ module Physical
72
137
  @properties = properties
73
138
  end
74
139
 
140
+ # Returns true if this location's address type is "residential"
141
+ # @return [Boolean]
75
142
  def residential?
76
143
  @address_type == 'residential'
77
144
  end
78
145
 
146
+ # Returns true if this location's address type is "commercial"
147
+ # @return [Boolean]
79
148
  def commercial?
80
149
  @address_type == 'commercial'
81
150
  end
82
151
 
152
+ # Returns true if this location's address type is "po_box"
153
+ # @return [Boolean]
83
154
  def po_box?
84
155
  @address_type == 'po_box'
85
156
  end
86
157
 
158
+ # Returns a hash representation of this location.
159
+ # @return [Hash]
87
160
  def to_hash
88
161
  {
89
162
  country: country&.code,
@@ -102,6 +175,8 @@ module Physical
102
175
  }
103
176
  end
104
177
 
178
+ # Returns true if the given object's class and {#to_hash} match this location.
179
+ # @return [Boolean]
105
180
  def ==(other)
106
181
  other.is_a?(self.class) &&
107
182
  to_hash == other&.to_hash
@@ -1,10 +1,46 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Physical
4
+ # Represents a physical package which has a container (box) and items.
4
5
  class Package
5
6
  extend Forwardable
6
- attr_reader :id, :container, :items, :void_fill_density, :items_weight, :used_volume, :description
7
7
 
8
+ # A unique identifier for this package
9
+ # @return [String]
10
+ attr_reader :id
11
+
12
+ # The container ({Box}) for this package
13
+ # @return [Physical::Cuboid]
14
+ attr_reader :container
15
+
16
+ # The items contained by this package
17
+ # @return [Array<Physical::Item>]
18
+ attr_reader :items
19
+
20
+ # The density of the void fill in this package
21
+ # @return [Measured::Density]
22
+ attr_reader :void_fill_density
23
+
24
+ # The weight of the items in this package
25
+ # @return [Measured::Weight]
26
+ attr_reader :items_weight
27
+
28
+ # The total volume used by the items in this package
29
+ # @return [Measured::Volume]
30
+ attr_reader :used_volume
31
+
32
+ # The description for this package
33
+ # @return [String]
34
+ attr_reader :description
35
+
36
+ # @param [String] id a unique identifier for this package
37
+ # @param [Physical::Cuboid] container the container ({Box}) for this package
38
+ # @param [Array<Physical::Item>] items the items contained by this package
39
+ # @param [Measured::Density] void_fill_density the density of the void fill in this package
40
+ # @param [Array<Measured::Length>] dimensions the length, width, and height of this package's container
41
+ # @param [Measured::Weight] weight the weight of this package's container
42
+ # @param [String] description a description for this package
43
+ # @param [Hash] properties additional custom properties for this package's container
8
44
  def initialize(id: nil, container: nil, items: [], void_fill_density: Measured::Density(0, :g_ml), dimensions: nil, weight: nil, description: nil, properties: {})
9
45
  @id = id || SecureRandom.uuid
10
46
  @void_fill_density = Types::Density[void_fill_density]
@@ -16,8 +52,22 @@ module Physical
16
52
  @used_volume = @items.map(&:volume).reduce(Measured::Volume(0, :ml), &:+)
17
53
  end
18
54
 
55
+ # @!attribute [r] dimensions
56
+ # The container's dimensions
57
+ # @!attribute [r] weight
58
+ # The container's weight
59
+ # @!attribute [r] length
60
+ # The container's length
61
+ # @!attribute [r] height
62
+ # The container's height
63
+ # @!attribute [r] properties
64
+ # The container's additional custom properties
65
+ # @!attribute [r] volume
66
+ # The container's volume
19
67
  delegate [:dimensions, :width, :length, :height, :properties, :volume] => :container
20
68
 
69
+ # Adds an item to the package.
70
+ # @param [Physical::Item] other the item to add
21
71
  def <<(other)
22
72
  @items.add(other)
23
73
  @items_weight += other.weight
@@ -25,6 +75,8 @@ module Physical
25
75
  end
26
76
  alias_method :add, :<<
27
77
 
78
+ # Removes an item from the package.
79
+ # @param [Physical::Item] other the item to remove
28
80
  def >>(other)
29
81
  @items.delete(other)
30
82
  @items_weight -= other.weight
@@ -32,28 +84,37 @@ module Physical
32
84
  end
33
85
  alias_method :delete, :>>
34
86
 
87
+ # Sums container weight, items weight, and void fill weight and returns the total.
88
+ # @return [Measured::Weight]
35
89
  def weight
36
90
  container.weight + items_weight + void_fill_weight
37
91
  end
38
92
 
39
- # Cost is optional. We will only return an aggregate if all items
40
- # have cost defined. Otherwise we will return nil.
41
- # @return Money
93
+ # Sums and returns the cost from all items in this package. Item cost is
94
+ # optional, therefore we only return a sum if *all* items have a cost.
95
+ # Otherwise, nil is returned.
96
+ # @return [Money, nil]
42
97
  def items_value
43
98
  items_cost = items.map(&:cost)
44
99
  items_cost.reduce(&:+) if items_cost.compact.size == items_cost.size
45
100
  end
46
101
 
102
+ # Calculates and returns the weight of the void fill in this package.
103
+ # @return [Measured::Weight]
47
104
  def void_fill_weight
48
105
  return Measured::Weight(0, :g) if container.volume.value.infinite?
49
106
 
50
107
  Measured::Weight(void_fill_density.convert_to(:g_ml).value * remaining_volume.convert_to(:ml).value, :g)
51
108
  end
52
109
 
110
+ # Calculates and returns remaining volume in this package.
111
+ # @return [Measured::Volume]
53
112
  def remaining_volume
54
113
  container.inner_volume - used_volume
55
114
  end
56
115
 
116
+ # Returns the density of this package based on its volume and weight.
117
+ # @return [Measured::Density]
57
118
  def density
58
119
  return Measured::Density(Float::INFINITY, :g_ml) if container.volume.value.zero?
59
120
  return Measured::Density(0.0, :g_ml) if container.volume.value.infinite?
@@ -3,24 +3,52 @@
3
3
  require 'measured'
4
4
 
5
5
  module Physical
6
+ # Represents a physical pallet which holds boxes.
6
7
  class Pallet < Cuboid
8
+ # The default dimensions of this pallet when unspecified
7
9
  DEFAULT_LENGTH = BigDecimal::INFINITY
10
+
11
+ # The default maximum weight of this pallet when unspecified
8
12
  DEFAULT_MAX_WEIGHT = BigDecimal::INFINITY
9
13
 
14
+ # The maximum weight this pallet can handle
15
+ # @return [Measured::Weight]
10
16
  attr_reader :max_weight
11
17
 
12
- def initialize(**args)
13
- max_weight = args.delete(:max_weight) || Measured::Weight(DEFAULT_MAX_WEIGHT, :g)
14
- super(**args)
18
+ # @param [Hash] kwargs ID, dimensions, weight, and properties
19
+ # @option kwargs [String] :id a unique identifier for this pallet
20
+ # @option kwargs [Array<Measured::Length>] :dimensions the length, width, and height of this pallet
21
+ # @option kwargs [Measured::Weight] :weight the weight of the pallet itself (excluding what's on top)
22
+ # @option kwargs [Measured::Weight] :max_weight the maximum weight this pallet can handle
23
+ # @option kwargs [Hash] :properties additional custom properties for this pallet
24
+ def initialize(**kwargs)
25
+ max_weight = kwargs.delete(:max_weight) || Measured::Weight(DEFAULT_MAX_WEIGHT, :g)
26
+ super(**kwargs)
15
27
  @max_weight = Types::Weight[max_weight]
16
28
  end
17
29
 
30
+ # @!method volume
31
+ # @return [Measured::Volume] the volume of this pallet
18
32
  alias_method :inner_volume, :volume
33
+
34
+ # @!method dimensions
35
+ # @return [Array<Measured::Length>] the dimensions of this pallet
19
36
  alias_method :inner_dimensions, :dimensions
37
+
38
+ # @!method length
39
+ # @return [Measured::Length] the length of this pallet
20
40
  alias_method :inner_length, :length
41
+
42
+ # @!method width
43
+ # @return [Measured::Length] the width of this pallet
21
44
  alias_method :inner_width, :width
45
+
46
+ # @!method height
47
+ # @return [Measured::Length] the height of this pallet
22
48
  alias_method :inner_height, :height
23
49
 
50
+ # Returns true if the given package can fit on the pallet. Checks package
51
+ # dimensions and weight against pallet dimensions and max weight.
24
52
  # @param [Physical::Package] package
25
53
  # @return [Boolean]
26
54
  def package_fits?(package)
@@ -1,16 +1,48 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Physical
4
+ # Represents a physical shipment containing structures and/or packages.
4
5
  class Shipment
5
- attr_reader :id,
6
- :origin,
7
- :destination,
8
- :service_code,
9
- :pallets,
10
- :structures,
11
- :packages,
12
- :options
6
+ # A unique identifier for this shipment
7
+ # @return [String]
8
+ attr_reader :id
13
9
 
10
+ # This shipment's origin location
11
+ # @return [Physical::Location]
12
+ attr_reader :origin
13
+
14
+ # This shipment's destination location
15
+ # @return [Physical::Location]
16
+ attr_reader :destination
17
+
18
+ # The shipment carrier's service code
19
+ # @return [String]
20
+ attr_reader :service_code
21
+
22
+ # This shipment's pallets (DEPRECATED: use {#structures} instead)
23
+ # @return [Array<Physical::Pallet>]
24
+ attr_reader :pallets
25
+
26
+ # This shipment's structures (pallets, skids, etc.) which hold packages
27
+ # @return [Array<Physical::Structure>]
28
+ attr_reader :structures
29
+
30
+ # This shipment's packages which hold items
31
+ # @return [Array<Physical::Package>]
32
+ attr_reader :packages
33
+
34
+ # Additional custom options for this shipment
35
+ # @return [Hash]
36
+ attr_reader :options
37
+
38
+ # @param [String] id a unique identifier for this shipment
39
+ # @param [Physical::Location] origin the shipment's origin location
40
+ # @param [Physical::Location] destination the shipment's destination location
41
+ # @param [String] service_code the shipment carrier's service code
42
+ # @param [Array<Physical::Pallet>] pallets the shipment's pallets (DEPRECATED: use `structures` instead)
43
+ # @param [Array<Physical::Structure>] structures the shipment's structures (pallets, skids, etc.) which hold packages
44
+ # @param [Array<Physical::Package>] packages the shipment's packages (boxes) which hold items
45
+ # @param [Hash] options additional custom options for this shipment
14
46
  def initialize(id: nil, origin: nil, destination: nil, service_code: nil, pallets: [], structures: [], packages: [], options: {})
15
47
  @id = id || SecureRandom.uuid
16
48
  @origin = origin
@@ -1,10 +1,36 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Physical
4
+ # Represents a physical structure which has a container (pallet, skid, etc.) and packages.
4
5
  class Structure
5
6
  extend Forwardable
6
- attr_reader :id, :container, :packages, :packages_weight, :used_volume
7
7
 
8
+ # A unique identifier for this structure
9
+ # @return [String]
10
+ attr_reader :id
11
+
12
+ # The container ({Pallet}) for this structure
13
+ # @return [Physical::Cuboid]
14
+ attr_reader :container
15
+
16
+ # The packages contained by this structure
17
+ # @return [Array<Physical::Package>]
18
+ attr_reader :packages
19
+
20
+ # The weight of the packages in this structure
21
+ # @return [Measured::Weight]
22
+ attr_reader :packages_weight
23
+
24
+ # The total volume used by the packages in this structure
25
+ # @return [Measured::Volume]
26
+ attr_reader :used_volume
27
+
28
+ # @param [String] id a unique identifier for this structure
29
+ # @param [Physical::Cuboid] container the container ({Pallet}) for this structure
30
+ # @param [Array<Physical::Package>] packages the packages contained by this structure
31
+ # @param [Array<Measured::Length>] dimensions the length, width, and height of this structure's container
32
+ # @param [Measured::Weight] weight the weight of this structure's container
33
+ # @param [Hash] properties additional custom properties for this package's container
8
34
  def initialize(id: nil, container: nil, packages: [], dimensions: nil, weight: nil, properties: {})
9
35
  @id = id || SecureRandom.uuid
10
36
  @container = container || Physical::Pallet.new(dimensions: dimensions || [], weight: weight || Measured::Weight(0, :g), properties: properties)
@@ -14,8 +40,22 @@ module Physical
14
40
  @used_volume = @packages.map(&:volume).reduce(Measured::Volume(0, :ml), &:+)
15
41
  end
16
42
 
43
+ # @!attribute [r] dimensions
44
+ # The container's dimensions
45
+ # @!attribute [r] weight
46
+ # The container's weight
47
+ # @!attribute [r] length
48
+ # The container's length
49
+ # @!attribute [r] height
50
+ # The container's height
51
+ # @!attribute [r] properties
52
+ # The container's additional custom properties
53
+ # @!attribute [r] volume
54
+ # The container's volume
17
55
  delegate [:dimensions, :width, :length, :height, :properties, :volume] => :container
18
56
 
57
+ # Adds a package to the structure.
58
+ # @param [Physical::Package] other the package to add
19
59
  def <<(other)
20
60
  @packages.add(other)
21
61
  @packages_weight += other.weight
@@ -23,6 +63,8 @@ module Physical
23
63
  end
24
64
  alias_method :add, :<<
25
65
 
66
+ # Removes a package from the structure.
67
+ # @param [Physical::Package] other the package to remove
26
68
  def >>(other)
27
69
  @packages.delete(other)
28
70
  @packages_weight -= other.weight
@@ -30,22 +72,29 @@ module Physical
30
72
  end
31
73
  alias_method :delete, :>>
32
74
 
75
+ # Sums container weight and packages weight and returns the total.
76
+ # @return [Measured::Weight]
33
77
  def weight
34
78
  container.weight + packages_weight
35
79
  end
36
80
 
37
- # Cost is optional. We will only return an aggregate if all packages
38
- # have items value defined. Otherwise we will return nil.
39
- # @return Money
81
+ # Sums and returns the item cost from all packages in this structure.
82
+ # Item cost is optional, therefore we only return a sum if *all* packages
83
+ # return item cost. Otherwise, nil is returned.
84
+ # @return [Money, nil]
40
85
  def packages_value
41
86
  packages_cost = packages.map(&:items_value)
42
87
  packages_cost.reduce(&:+) if packages_cost.compact.size == packages_cost.size
43
88
  end
44
89
 
90
+ # Calculates and returns remaining volume in this structure.
91
+ # @return [Measured::Volume]
45
92
  def remaining_volume
46
93
  container.inner_volume - used_volume
47
94
  end
48
95
 
96
+ # Returns the density of this structure based on its volume and weight.
97
+ # @return [Measured::Density]
49
98
  def density
50
99
  return Measured::Density(Float::INFINITY, :g_ml) if container.volume.value.zero?
51
100
  return Measured::Density(0.0, :g_ml) if container.volume.value.infinite?
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Physical
4
- VERSION = "0.5.1"
4
+ VERSION = "0.6.0"
5
5
  end
data/physical.gemspec CHANGED
@@ -19,17 +19,20 @@ Gem::Specification.new do |spec|
19
19
  f.match(%r{^(spec)/})
20
20
  end
21
21
  spec.require_paths = ["lib"]
22
- spec.required_ruby_version = '>= 2.4'
22
+ spec.required_ruby_version = '>= 3.0'
23
23
  spec.add_runtime_dependency "carmen", "~> 1.0"
24
24
  spec.add_runtime_dependency "dry-types", "~> 1.5"
25
- spec.add_runtime_dependency "measured", "~> 2.4"
25
+ spec.add_runtime_dependency "measured", "~> 3.0"
26
26
  spec.add_runtime_dependency "money", ">= 5"
27
27
 
28
28
  spec.add_development_dependency "bundler", [">= 1.16", "< 3"]
29
29
  spec.add_development_dependency "factory_bot", "~> 6.2"
30
+ spec.add_development_dependency "rack"
30
31
  spec.add_development_dependency "rake", ">= 12.3.3"
31
32
  spec.add_development_dependency "rspec", "~> 3.0"
32
33
  spec.add_development_dependency "rspec_junit_formatter", "~> 0.4"
33
34
  spec.add_development_dependency "rubocop"
34
35
  spec.add_development_dependency "simplecov"
36
+ spec.add_development_dependency "webrick"
37
+ spec.add_development_dependency "yard"
35
38
  end
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: physical
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.1
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Martin Meyerhoff
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2023-12-20 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: carmen
@@ -44,14 +43,14 @@ dependencies:
44
43
  requirements:
45
44
  - - "~>"
46
45
  - !ruby/object:Gem::Version
47
- version: '2.4'
46
+ version: '3.0'
48
47
  type: :runtime
49
48
  prerelease: false
50
49
  version_requirements: !ruby/object:Gem::Requirement
51
50
  requirements:
52
51
  - - "~>"
53
52
  - !ruby/object:Gem::Version
54
- version: '2.4'
53
+ version: '3.0'
55
54
  - !ruby/object:Gem::Dependency
56
55
  name: money
57
56
  requirement: !ruby/object:Gem::Requirement
@@ -100,6 +99,20 @@ dependencies:
100
99
  - - "~>"
101
100
  - !ruby/object:Gem::Version
102
101
  version: '6.2'
102
+ - !ruby/object:Gem::Dependency
103
+ name: rack
104
+ requirement: !ruby/object:Gem::Requirement
105
+ requirements:
106
+ - - ">="
107
+ - !ruby/object:Gem::Version
108
+ version: '0'
109
+ type: :development
110
+ prerelease: false
111
+ version_requirements: !ruby/object:Gem::Requirement
112
+ requirements:
113
+ - - ">="
114
+ - !ruby/object:Gem::Version
115
+ version: '0'
103
116
  - !ruby/object:Gem::Dependency
104
117
  name: rake
105
118
  requirement: !ruby/object:Gem::Requirement
@@ -170,6 +183,34 @@ dependencies:
170
183
  - - ">="
171
184
  - !ruby/object:Gem::Version
172
185
  version: '0'
186
+ - !ruby/object:Gem::Dependency
187
+ name: webrick
188
+ requirement: !ruby/object:Gem::Requirement
189
+ requirements:
190
+ - - ">="
191
+ - !ruby/object:Gem::Version
192
+ version: '0'
193
+ type: :development
194
+ prerelease: false
195
+ version_requirements: !ruby/object:Gem::Requirement
196
+ requirements:
197
+ - - ">="
198
+ - !ruby/object:Gem::Version
199
+ version: '0'
200
+ - !ruby/object:Gem::Dependency
201
+ name: yard
202
+ requirement: !ruby/object:Gem::Requirement
203
+ requirements:
204
+ - - ">="
205
+ - !ruby/object:Gem::Version
206
+ version: '0'
207
+ type: :development
208
+ prerelease: false
209
+ version_requirements: !ruby/object:Gem::Requirement
210
+ requirements:
211
+ - - ">="
212
+ - !ruby/object:Gem::Version
213
+ version: '0'
173
214
  description: A package with boxes and items
174
215
  email:
175
216
  - mamhoff@gmail.com
@@ -183,6 +224,7 @@ files:
183
224
  - ".rubocop-relaxed.yml"
184
225
  - ".rubocop.yml"
185
226
  - ".travis.yml"
227
+ - ".yardopts"
186
228
  - CHANGELOG.md
187
229
  - CODE_OF_CONDUCT.md
188
230
  - Gemfile
@@ -220,7 +262,6 @@ homepage: https://github.com/friendlycart/physical
220
262
  licenses:
221
263
  - MIT
222
264
  metadata: {}
223
- post_install_message:
224
265
  rdoc_options: []
225
266
  require_paths:
226
267
  - lib
@@ -228,15 +269,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
228
269
  requirements:
229
270
  - - ">="
230
271
  - !ruby/object:Gem::Version
231
- version: '2.4'
272
+ version: '3.0'
232
273
  required_rubygems_version: !ruby/object:Gem::Requirement
233
274
  requirements:
234
275
  - - ">="
235
276
  - !ruby/object:Gem::Version
236
277
  version: '0'
237
278
  requirements: []
238
- rubygems_version: 3.4.22
239
- signing_key:
279
+ rubygems_version: 3.7.1
240
280
  specification_version: 4
241
281
  summary: A facade to deal with physical packages
242
282
  test_files: []